Repository: toeverything/AFFiNE
Pull request #11246: refactor(editor): inner toolbar surface-ref block with extension
Author: login: L-Sun
Labels: name: mod:i18n
description: Related to i18n
name: test
description: Related to test cases
name: app:core
Created: 2025-03-27T11:07:56Z | Merged: 2025-03-28T06:00:30Z

This PR refactor `surface-ref` toolbar with `ToolbarExtension`


Diff:
diff --git a/blocksuite/affine/blocks/block-surface-ref/package.json b/blocksuite/affine/blocks/block-surface-ref/package.json
index e8f06e1b8da21..a0664d90fee7f 100644
--- a/blocksuite/affine/blocks/block-surface-ref/package.json
+++ b/blocksuite/affine/blocks/block-surface-ref/package.json
@@ -13,6 +13,7 @@
     "@blocksuite/affine-block-frame": "workspace:*",
     "@blocksuite/affine-block-surface": "workspace:*",
     "@blocksuite/affine-components": "workspace:*",
+    "@blocksuite/affine-inline-reference": "workspace:*",
     "@blocksuite/affine-model": "workspace:*",
     "@blocksuite/affine-shared": "workspace:*",
     "@blocksuite/affine-widget-slash-menu": "workspace:*",
diff --git a/blocksuite/affine/blocks/block-surface-ref/src/components/index.ts b/blocksuite/affine/blocks/block-surface-ref/src/components/index.ts
new file mode 100644
index 0000000000000..284bdca8b468b
--- /dev/null
+++ b/blocksuite/affine/blocks/block-surface-ref/src/components/index.ts
@@ -0,0 +1 @@
+export { SurfaceRefToolbarTitle } from './surface-ref-toolbar-title';
diff --git a/blocksuite/affine/blocks/block-surface-ref/src/components/surface-ref-toolbar-title.ts b/blocksuite/affine/blocks/block-surface-ref/src/components/surface-ref-toolbar-title.ts
new file mode 100644
index 0000000000000..40c704069afdf
--- /dev/null
+++ b/blocksuite/affine/blocks/block-surface-ref/src/components/surface-ref-toolbar-title.ts
@@ -0,0 +1,68 @@
+import {
+  FrameBlockModel,
+  GroupElementModel,
+  MindmapElementModel,
+  ShapeElementModel,
+} from '@blocksuite/affine-model';
+import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
+import { ShadowlessElement } from '@blocksuite/block-std';
+import type { GfxModel } from '@blocksuite/block-std/gfx';
+import {
+  EdgelessIcon,
+  FrameIcon,
+  GroupIcon,
+  MindmapIcon,
+} from '@blocksuite/icons/lit';
+import { css, html, type TemplateResult } from 'lit';
+import { property } from 'lit/decorators.js';
+
+export class SurfaceRefToolbarTitle extends ShadowlessElement {
+  static override styles = css`
+    surface-ref-toolbar-title {
+      display: flex;
+      padding: 2px 4px;
+      margin-right: auto;
+      align-items: center;
+      gap: 4px;
+      border-radius: 2px;
+      background: ${unsafeCSSVarV2('button/iconButtonSolid')};
+
+      svg {
+        color: ${unsafeCSSVarV2('icon/primary')};
+        width: 16px;
+        height: 16px;
+      }
+
+      span {
+        color: ${unsafeCSSVarV2('text/primary')};
+        font-size: 12px;
+        font-weight: 500;
+        line-height: 20px;
+      }
+    }
+  `;
+
+  @property({ attribute: false })
+  accessor referenceModel: GfxModel | null = null;
+
+  override render() {
+    const { referenceModel } = this;
+    let title = '';
+    let icon: TemplateResult = EdgelessIcon();
+    if (referenceModel instanceof GroupElementModel) {
+      title = referenceModel.title.toString();
+      icon = GroupIcon();
+    } else if (referenceModel instanceof FrameBlockModel) {
+      title = referenceModel.props.title.toString();
+      icon = FrameIcon();
+    } else if (referenceModel instanceof MindmapElementModel) {
+      const rootElement = referenceModel.tree.element;
+      if (rootElement instanceof ShapeElementModel) {
+        title = rootElement.text?.toString() ?? '';
+      }
+      icon = MindmapIcon();
+    }
+
+    return html`${icon}<span>${title}</span>`;
+  }
+}
diff --git a/blocksuite/affine/blocks/block-surface-ref/src/configs/toolbar.ts b/blocksuite/affine/blocks/block-surface-ref/src/configs/toolbar.ts
new file mode 100644
index 0000000000000..b890208af0434
--- /dev/null
+++ b/blocksuite/affine/blocks/block-surface-ref/src/configs/toolbar.ts
@@ -0,0 +1,125 @@
+import { toast } from '@blocksuite/affine-components/toast';
+import {
+  copySelectedModelsCommand,
+  draftSelectedModelsCommand,
+} from '@blocksuite/affine-shared/commands';
+import {
+  ActionPlacement,
+  type ToolbarModuleConfig,
+} from '@blocksuite/affine-shared/services';
+import { CaptionIcon, CopyIcon, DeleteIcon } from '@blocksuite/icons/lit';
+import { html } from 'lit';
+
+import { SurfaceRefBlockComponent } from '../surface-ref-block';
+import { surfaceRefToBlob, writeImageBlobToClipboard } from '../utils';
+
+export const surfaceRefToolbarModuleConfig: ToolbarModuleConfig = {
+  actions: [
+    {
+      id: 'a.surface-ref-title',
+      content: ctx => {
+        const surfaceRefBlock = ctx.getCurrentBlockByType(
+          SurfaceRefBlockComponent
+        );
+        if (!surfaceRefBlock) return null;
+
+        return html`<surface-ref-toolbar-title
+          .referenceModel=${surfaceRefBlock.referenceModel}
+        ></surface-ref-toolbar-title>`;
+      },
+    },
+    {
+      id: 'c.copy-surface-ref',
+      label: 'Copy',
+      icon: CopyIcon(),
+      run: ctx => {
+        const surfaceRefBlock = ctx.getCurrentBlockByType(
+          SurfaceRefBlockComponent
+        );
+        if (!surfaceRefBlock) return;
+
+        ctx.chain
+          .pipe(draftSelectedModelsCommand, {
+            selectedModels: [surfaceRefBlock.model],
+          })
+          .pipe(copySelectedModelsCommand)
+          .run();
+
+        toast(surfaceRefBlock.std.host, 'Copied to clipboard');
+      },
+    },
+    {
+      id: 'd.surface-ref-caption',
+      icon: CaptionIcon(),
+      run: ctx => {
+        const surfaceRefBlock = ctx.getCurrentBlockByType(
+          SurfaceRefBlockComponent
+        );
+        if (!surfaceRefBlock) return;
+
+        surfaceRefBlock.captionElement.show();
+      },
+    },
+    {
+      id: 'a.clipboard',
+      placement: ActionPlacement.More,
+      when: ctx => {
+        const surfaceRefBlock = ctx.getCurrentBlock();
+        if (!(surfaceRefBlock instanceof SurfaceRefBlockComponent))
+          return false;
+
+        return !!surfaceRefBlock.referenceModel;
+      },
+      actions: [
+        {
+          id: 'a.surface-ref-copy-as-image',
+          label: 'Copy as Image',
+          icon: CopyIcon(),
+          placement: ActionPlacement.More,
+          when: ctx => {
+            const surfaceRefBlock = ctx.getCurrentBlockByType(
+              SurfaceRefBlockComponent
+            );
+            if (!surfaceRefBlock) return false;
+
+            return !!surfaceRefBlock.referenceModel;
+          },
+          run: ctx => {
+            const surfaceRefBlock = ctx.getCurrentBlockByType(
+              SurfaceRefBlockComponent
+            );
+            if (!surfaceRefBlock) return;
+
+            surfaceRefToBlob(surfaceRefBlock)
+              .then(async blob => {
+                if (!blob) {
+                  toast(ctx.host, 'Failed to render surface-ref to image');
+                } else {
+                  await writeImageBlobToClipboard(blob);
+                  toast(ctx.host, 'Copied image to clipboard');
+                }
+              })
+              .catch(console.error);
+          },
+        },
+        // TODO(@L-Sun): add duplicate action after refactoring root-block/edgeless
+      ],
+    },
+    {
+      id: 'g.surface-ref-deletion',
+      label: 'Delete',
+      icon: DeleteIcon(),
+      placement: ActionPlacement.More,
+      variant: 'destructive',
+      run: ctx => {
+        const surfaceRefBlock = ctx.getCurrentBlockByType(
+          SurfaceRefBlockComponent
+        );
+        if (!surfaceRefBlock) return;
+
+        ctx.store.deleteBlock(surfaceRefBlock.model);
+      },
+    },
+  ],
+  placement: 'inner',
+};
diff --git a/blocksuite/affine/blocks/block-surface-ref/src/effects.ts b/blocksuite/affine/blocks/block-surface-ref/src/effects.ts
index 5b4de41f76bcc..27bf90ea8758e 100644
--- a/blocksuite/affine/blocks/block-surface-ref/src/effects.ts
+++ b/blocksuite/affine/blocks/block-surface-ref/src/effects.ts
@@ -1,11 +1,8 @@
-import { SurfaceRefGenericBlockPortal } from './portal/generic-block.js';
-import { SurfaceRefNotePortal } from './portal/note.js';
-import { SurfaceRefBlockComponent } from './surface-ref-block.js';
-import { EdgelessSurfaceRefBlockComponent } from './surface-ref-block-edgeless.js';
-import {
-  AFFINE_SURFACE_REF_TOOLBAR,
-  AffineSurfaceRefToolbar,
-} from './widgets/surface-ref-toolbar.js';
+import { SurfaceRefToolbarTitle } from './components';
+import { SurfaceRefGenericBlockPortal } from './portal/generic-block';
+import { SurfaceRefNotePortal } from './portal/note';
+import { SurfaceRefBlockComponent } from './surface-ref-block';
+import { EdgelessSurfaceRefBlockComponent } from './surface-ref-block-edgeless';
 
 export function effects() {
   customElements.define(
@@ -18,5 +15,11 @@ export function effects() {
     EdgelessSurfaceRefBlockComponent
   );
   customElements.define('surface-ref-note-portal', SurfaceRefNotePortal);
-  customElements.define(AFFINE_SURFACE_REF_TOOLBAR, AffineSurfaceRefToolbar);
+  customElements.define('surface-ref-toolbar-title', SurfaceRefToolbarTitle);
+}
+
+declare global {
+  interface HTMLElementTagNameMap {
+    'surface-ref-toolbar-title': SurfaceRefToolbarTitle;
+  }
 }
diff --git a/blocksuite/affine/blocks/block-surface-ref/src/surface-ref-block.ts b/blocksuite/affine/blocks/block-surface-ref/src/surface-ref-block.ts
index e93ff0ff6b238..2340e0cc091ef 100644
--- a/blocksuite/affine/blocks/block-surface-ref/src/surface-ref-block.ts
+++ b/blocksuite/affine/blocks/block-surface-ref/src/surface-ref-block.ts
@@ -4,7 +4,9 @@ import {
   getSurfaceBlock,
 } from '@blocksuite/affine-block-surface';
 import type { BlockCaptionEditor } from '@blocksuite/affine-components/caption';
+import { whenHover } from '@blocksuite/affine-components/hover';
 import { Peekable } from '@blocksuite/affine-components/peek';
+import { RefNodeSlotsProvider } from '@blocksuite/affine-inline-reference';
 import {
   FrameBlockModel,
   type SurfaceRefBlockModel,
@@ -12,7 +14,9 @@ import {
 import {
   DocModeProvider,
   EditPropsStore,
+  type OpenDocMode,
   ThemeProvider,
+  ToolbarRegistryIdentifier,
   ViewportElementExtension,
 } from '@blocksuite/affine-shared/services';
 import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
@@ -424,6 +428,29 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
     this._previewSpec.extend([SurfaceRefViewportWatcher]);
   }
 
+  private _initHover() {
+    const { setReference, setFloating, dispose } = whenHover(
+      hovered => {
+        const message$ = this.std.get(ToolbarRegistryIdentifier).message$;
+        if (hovered) {
+          message$.value = {
+            flavour: this.model.flavour,
+            element: this,
+            setFloating,
+          };
+          return;
+        }
+
+        // Clears previous bindings
+        message$.value = null;
+        setFloating();
+      },
+      { enterDelay: 500 }
+    );
+    setReference(this);
+    this._disposables.add(dispose);
+  }
+
   private _renderRefContent(referencedModel: GfxModel) {
     const [, , w, h] = deserializeXYWH(referencedModel.xywh);
     const _previewSpec = this._previewSpec.value;
@@ -468,6 +495,28 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
     </div>`;
   }
 
+  readonly open = ({
+    openMode,
+    event,
+  }: {
+    openMode?: OpenDocMode;
+    event?: MouseEvent;
+  } = {}) => {
+    const pageId = this.referenceModel?.surface?.doc.id;
+    if (!pageId) return;
+
+    this.std.getOptional(RefNodeSlotsProvider)?.docLinkClicked.next({
+      pageId: pageId,
+      params: {
+        mode: 'edgeless',
+        elementIds: [this.model.props.reference],
+      },
+      openMode,
+      event,
+      host: this.host,
+    });
+  };
+
   override connectedCallback() {
     super.connectedCallback();
 
@@ -479,6 +528,7 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
     this._initViewport();
     this._initReferencedModel();
     this._initSelection();
+    this._initHover();
   }
 
   override render() {
diff --git a/blocksuite/affine/blocks/block-surface-ref/src/surface-ref-spec.ts b/blocksuite/affine/blocks/block-surface-ref/src/surface-ref-spec.ts
index 74cc659bf2de5..0a16ed768d664 100644
--- a/blocksuite/affine/blocks/block-surface-ref/src/surface-ref-spec.ts
+++ b/blocksuite/affine/blocks/block-surface-ref/src/surface-ref-spec.ts
@@ -1,30 +1,33 @@
+import { SurfaceRefBlockSchema } from '@blocksuite/affine-model';
+import { ToolbarModuleExtension } from '@blocksuite/affine-shared/services';
 import {
+  BlockFlavourIdentifier,
   BlockViewExtension,
   FlavourExtension,
-  WidgetViewExtension,
 } from '@blocksuite/block-std';
 import type { ExtensionType } from '@blocksuite/store';
 import { literal } from 'lit/static-html.js';
 
 import { SurfaceRefSlashMenuConfigExtension } from './configs/slash-menu';
-
-export const surfaceRefToolbarWidget = WidgetViewExtension(
-  'affine:surface-ref',
-  'surfaceToolbar',
-  literal`affine-surface-ref-toolbar`
-);
+import { surfaceRefToolbarModuleConfig } from './configs/toolbar';
 
 export const PageSurfaceRefBlockSpec: ExtensionType[] = [
-  FlavourExtension('affine:surface-ref'),
-  BlockViewExtension('affine:surface-ref', literal`affine-surface-ref`),
-  surfaceRefToolbarWidget,
+  FlavourExtension(SurfaceRefBlockSchema.model.flavour),
+  BlockViewExtension(
+    SurfaceRefBlockSchema.model.flavour,
+    literal`affine-surface-ref`
+  ),
+  ToolbarModuleExtension({
+    id: BlockFlavourIdentifier(SurfaceRefBlockSchema.model.flavour),
+    config: surfaceRefToolbarModuleConfig,
+  }),
   SurfaceRefSlashMenuConfigExtension,
 ];
 
 export const EdgelessSurfaceRefBlockSpec: ExtensionType[] = [
-  FlavourExtension('affine:surface-ref'),
+  FlavourExtension(SurfaceRefBlockSchema.model.flavour),
   BlockViewExtension(
-    'affine:surface-ref',
+    SurfaceRefBlockSchema.model.flavour,
     literal`affine-edgeless-surface-ref`
   ),
   SurfaceRefSlashMenuConfigExtension,
diff --git a/blocksuite/affine/blocks/block-surface-ref/src/utils.ts b/blocksuite/affine/blocks/block-surface-ref/src/utils.ts
index 238c931e939b6..e13a6da927752 100644
--- a/blocksuite/affine/blocks/block-surface-ref/src/utils.ts
+++ b/blocksuite/affine/blocks/block-surface-ref/src/utils.ts
@@ -1,3 +1,10 @@
+import type { SurfaceBlockComponent } from '@blocksuite/affine-block-surface';
+import { ExportManager } from '@blocksuite/affine-block-surface';
+import type { SurfaceRefBlockComponent } from '@blocksuite/affine-block-surface-ref';
+import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
+import { BlockSuiteError } from '@blocksuite/global/exceptions';
+import { Bound } from '@blocksuite/global/gfx';
+import { assertType } from '@blocksuite/global/utils';
 import { html } from 'lit';
 
 export const noContentPlaceholder = html`
@@ -97,3 +104,43 @@ export const noContentPlaceholder = html`
     />
   </svg>
 `;
+
+export const surfaceRefToBlob = async (
+  surfaceRefBlock: SurfaceRefBlockComponent
+): Promise<Blob | null> => {
+  const { referenceModel, previewEditor } = surfaceRefBlock;
+  if (!referenceModel || !previewEditor) return null;
+
+  const exportManager = previewEditor.std.get(ExportManager);
+  const gfx = previewEditor.std.get(GfxControllerIdentifier);
+
+  const { surface } = gfx;
+  if (!surface) return null;
+  const surfaceBlock = previewEditor.std.view.getBlock(surface.id);
+  if (!surfaceBlock) return null;
+  assertType<SurfaceBlockComponent>(surfaceBlock);
+
+  const canvas = await exportManager.edgelessToCanvas(
+    surfaceBlock.renderer,
+    Bound.deserialize(referenceModel.xywh),
+    gfx,
+    undefined,
+    undefined,
+    { zoom: surfaceBlock.renderer.viewport.zoom }
+  );
+
+  if (!canvas) {
+    throw new BlockSuiteError(
+      BlockSuiteError.ErrorCode.ValueNotExists,
+      'Failed to export edgeless to canvas'
+    );
+  }
+
+  return new Promise((resolve, reject) => {
+    canvas.toBlob(blob => (blob ? resolve(blob) : reject(null)), 'image/png');
+  });
+};
+
+export const writeImageBlobToClipboard = async (blob: Blob) => {
+  await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]);
+};
diff --git a/blocksuite/affine/blocks/block-surface-ref/src/widgets/config.ts b/blocksuite/affine/blocks/block-surface-ref/src/widgets/config.ts
deleted file mode 100644
index 2365477797663..0000000000000
--- a/blocksuite/affine/blocks/block-surface-ref/src/widgets/config.ts
+++ /dev/null
@@ -1,122 +0,0 @@
-import { type SurfaceBlockComponent } from '@blocksuite/affine-block-surface';
-import {
-  CopyIcon,
-  DeleteIcon,
-  DownloadIcon,
-} from '@blocksuite/affine-components/icons';
-import { toast } from '@blocksuite/affine-components/toast';
-import type { MenuItemGroup } from '@blocksuite/affine-components/toolbar';
-import { downloadBlob } from '@blocksuite/affine-shared/utils';
-import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
-
-import type { SurfaceRefToolbarContext } from './context.js';
-import { edgelessToBlob, writeImageBlobToClipboard } from './utils.js';
-
-export const BUILT_IN_GROUPS: MenuItemGroup<SurfaceRefToolbarContext>[] = [
-  {
-    type: 'clipboard',
-    when: ctx => !!(ctx.blockComponent.referenceModel && ctx.doc.root),
-    items: [
-      {
-        type: 'copy',
-        label: 'Copy as Image',
-        icon: CopyIcon,
-        action: ctx => {
-          if (!(ctx.blockComponent.referenceModel && ctx.doc.root?.id)) {
-            ctx.close();
-            return;
-          }
-
-          const referencedModel = ctx.blockComponent.referenceModel;
-          const editor = ctx.blockComponent.previewEditor;
-          const surfaceModel = editor?.std.get(GfxControllerIdentifier).surface;
-          if (!surfaceModel) {
-            ctx.close();
-            return;
-          }
-          const surfaceBlock = editor?.std.view.getBlock(
-            surfaceModel.id
-          ) as SurfaceBlockComponent;
-          const surfaceRenderer = surfaceBlock.renderer;
-
-          if (!surfaceRenderer) {
-            ctx.close();
-            return;
-          }
-
-          edgelessToBlob(editor, {
-            surfaceRefBlock: ctx.blockComponent,
-            surfaceRenderer,
-            edgelessElement: referencedModel,
-          })
-            .then(blob => writeImageBlobToClipboard(blob))
-            .then(() => toast(ctx.host, 'Copied image to clipboard'))
-            .catch(console.error);
-
-          ctx.close();
-        },
-      },
-      // TODO(@L-Sun): add duplicate action after refactoring toolbar
-      {
-        type: 'download',
-        label: 'Download',
-        icon: DownloadIcon,
-        action: ctx => {
-          if (!(ctx.blockComponent.referenceModel && ctx.doc.root?.id)) {
-            ctx.close();
-            return;
-          }
-
-          const referencedModel = ctx.blockComponent.referenceModel;
-          const editor = ctx.blockComponent.previewEditor;
-          const surfaceModel = editor?.std.get(GfxControllerIdentifier).surface;
-          if (!surfaceModel) {
-            ctx.close();
-            return;
-          }
-          const surfaceBlock = editor?.std.view.getBlock(
-            surfaceModel.id
-          ) as SurfaceBlockComponent;
-          const surfaceRenderer = surfaceBlock.renderer;
-
-          if (!surfaceRenderer) {
-            ctx.close();
-            return;
-          }
-
-          edgelessToBlob(editor, {
-            surfaceRefBlock: ctx.blockComponent,
-            surfaceRenderer,
-            edgelessElement: referencedModel,
-          })
-            .then(blob => {
-              const fileName =
-                'title' in referencedModel
-                  ? (referencedModel.title?.toString() ?? 'Edgeless Content')
-                  : 'Edgeless Content';
-
-              downloadBlob(blob, fileName);
-            })
-            .catch(console.error);
-
-          ctx.close();
-        },
-      },
-    ],
-  },
-  {
-    type: 'delete',
-    items: [
-      {
-        type: 'delete',
-        label: 'Delete',
-        icon: DeleteIcon,
-        disabled: ({ doc }) => doc.readonly,
-        action: ({ blockComponent, doc, close }) => {
-          doc.deleteBlock(blockComponent.model);
-          close();
-        },
-      },
-    ],
-  },
-];
diff --git a/blocksuite/affine/blocks/block-surface-ref/src/widgets/context.ts b/blocksuite/affine/blocks/block-surface-ref/src/widgets/context.ts
deleted file mode 100644
index 5c67e3d4931b5..0000000000000
--- a/blocksuite/affine/blocks/block-surface-ref/src/widgets/context.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import type { SurfaceRefBlockComponent } from '@blocksuite/affine-block-surface-ref';
-import { MenuContext } from '@blocksuite/affine-components/toolbar';
-
-export class SurfaceRefToolbarContext extends MenuContext {
-  override close = () => {
-    this.abortController.abort();
-  };
-
-  get doc() {
-    return this.blockComponent.doc;
-  }
-
-  get host() {
-    return this.blockComponent.host;
-  }
-
-  get selectedBlockModels() {
-    if (this.blockComponent) return [this.blockComponent.model];
-    return [];
-  }
-
-  get std() {
-    return this.host.std;
-  }
-
-  constructor(
-    public blockComponent: SurfaceRefBlockComponent,
-    public abortController: AbortController
-  ) {
-    super();
-  }
-
-  isEmpty() {
-    return !this.blockComponent;
-  }
-
-  isMultiple() {
-    return false;
-  }
-
-  isSingle() {
-    return true;
-  }
-}
diff --git a/blocksuite/affine/blocks/block-surface-ref/src/widgets/surface-ref-toolbar.ts b/blocksuite/affine/blocks/block-surface-ref/src/widgets/surface-ref-toolbar.ts
deleted file mode 100644
index f14189afc8d01..0000000000000
--- a/blocksuite/affine/blocks/block-surface-ref/src/widgets/surface-ref-toolbar.ts
+++ /dev/null
@@ -1,280 +0,0 @@
-import type { SurfaceRefBlockComponent } from '@blocksuite/affine-block-surface-ref';
-import { peek } from '@blocksuite/affine-components/peek';
-import { toast } from '@blocksuite/affine-components/toast';
-import {
-  cloneGroups,
-  getMoreMenuConfig,
-  type MenuItem,
-  type MenuItemGroup,
-  renderGroups,
-} from '@blocksuite/affine-components/toolbar';
-import {
-  FrameBlockModel,
-  GroupElementModel,
-  MindmapElementModel,
-  ShapeElementModel,
-  type SurfaceRefBlockModel,
-} from '@blocksuite/affine-model';
-import {
-  copySelectedModelsCommand,
-  draftSelectedModelsCommand,
-} from '@blocksuite/affine-shared/commands';
-import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
-import type { ButtonPopperOptions } from '@blocksuite/affine-shared/utils';
-import { WidgetComponent } from '@blocksuite/block-std';
-import {
-  ArrowDownSmallIcon,
-  CaptionIcon,
-  CenterPeekIcon,
-  CopyIcon,
-  EdgelessIcon,
-  FrameIcon,
-  GroupIcon,
-  MindmapIcon,
-  MoreVerticalIcon,
-  OpenInNewIcon,
-} from '@blocksuite/icons/lit';
-import { css, html, nothing, type TemplateResult } from 'lit';
-import { ifDefined } from 'lit/directives/if-defined.js';
-import { repeat } from 'lit/directives/repeat.js';
-import { styleMap } from 'lit/directives/style-map.js';
-import { when } from 'lit/directives/when.js';
-
-import { BUILT_IN_GROUPS } from './config.js';
-import { SurfaceRefToolbarContext } from './context.js';
-
-export const AFFINE_SURFACE_REF_TOOLBAR = 'affine-surface-ref-toolbar';
-
-export class AffineSurfaceRefToolbar extends WidgetComponent<
-  SurfaceRefBlockModel,
-  SurfaceRefBlockComponent
-> {
-  static override styles = css`
-    :host {
-      position: absolute;
-      top: 0;
-      left: 0;
-
-      width: 100%;
-      gap: 4px;
-      padding: 4px;
-      margin: 0;
-      display: flex;
-      justify-content: flex-end;
-
-      editor-icon-button,
-      editor-menu-button {
-        background: ${unsafeCSSVarV2('button/iconButtonSolid')};
-        color: ${unsafeCSSVarV2('text/primary')};
-        box-shadow: ${unsafeCSSVar('shadow1')};
-        border-radius: 4px;
-      }
-    }
-
-    .surface-ref-toolbar-title {
-      display: flex;
-      padding: 2px 4px;
-      margin-right: auto;
-      align-items: center;
-      gap: 4px;
-      border-radius: 2px;
-      background: ${unsafeCSSVarV2('button/iconButtonSolid')};
-
-      svg {
-        color: ${unsafeCSSVarV2('icon/primary')};
-        width: 16px;
-        height: 16px;
-      }
-
-      span {
-        color: ${unsafeCSSVarV2('text/primary')};
-        font-size: 12px;
-        font-weight: 500;
-        line-height: 20px;
-      }
-    }
-  `;
-
-  private readonly _popoverOptions: Partial<ButtonPopperOptions> = {
-    mainAxis: 4,
-    stateUpdated: ({ display }) => {
-      this.dataset.openMenuDisplay = display;
-    },
-  };
-
-  /*
-   * Caches the more menu items.
-   * Currently only supports configuring more menu.
-   */
-  moreGroups: MenuItemGroup<SurfaceRefToolbarContext>[] =
-    cloneGroups(BUILT_IN_GROUPS);
-
-  override connectedCallback() {
-    super.connectedCallback();
-    this.moreGroups = getMoreMenuConfig(this.std).configure(this.moreGroups);
-
-    this.disposables.addFromEvent(this, 'dblclick', e => {
-      e.stopPropagation();
-    });
-  }
-
-  private _renderTitle() {
-    if (!this.block) return nothing;
-    const { referenceModel } = this.block;
-    if (!referenceModel) return nothing;
-
-    let title = '';
-    let icon: TemplateResult<1> | null = null;
-    if (referenceModel instanceof GroupElementModel) {
-      title = referenceModel.title.toString();
-      icon = GroupIcon();
-    } else if (referenceModel instanceof FrameBlockModel) {
-      title = referenceModel.props.title.toString();
-      icon = FrameIcon();
-    } else if (referenceModel instanceof MindmapElementModel) {
-      const rootElement = referenceModel.tree.element;
-      if (rootElement instanceof ShapeElementModel) {
-        title = rootElement.text?.toString() ?? '';
-      }
-      icon = MindmapIcon();
-    }
-
-    return html`<div class="surface-ref-toolbar-title">
-      ${icon}
-      <span>${title}</span>
-    </div>`;
-  }
-
-  private _renderOpenButton() {
-    const referenceModel = this.block?.referenceModel;
-    if (!referenceModel) return nothing;
-
-    const openMenuActions: MenuItem[] = [
-      {
-        type: 'open-in-edgeless',
-        label: 'Open in Edgeless',
-        icon: EdgelessIcon(),
-        action: () => this.block?.viewInEdgeless(),
-        disabled: this.block.model.doc.readonly,
-      },
-      {
-        type: 'open-in-center-peek',
-        label: 'Open in center peek',
-        icon: CenterPeekIcon(),
-        action: () => this.block && peek(this.block),
-      },
-      // TODO(@L-Sun): add split view and new tab
-    ];
-
-    return html`<editor-menu-button
-      data-show
-      aria-label="Open"
-      style=${styleMap({
-        '--content-padding': '8px',
-      })}
-      .button=${html`
-        <editor-icon-button .iconContainerPadding=${4} .iconSize=${'16px'}>
-          ${OpenInNewIcon()} ${ArrowDownSmallIcon()}
-        </editor-icon-button>
-      `}
-      .popperOptions=${this._popoverOptions}
-    >
-      <div data-orientation="vertical">
-        ${repeat(
-          openMenuActions,
-          button => button.label,
-          ({ label, icon, action, disabled }) => html`
-            <editor-menu-action
-              aria-label=${ifDefined(label)}
-              ?disabled=${disabled}
-              @click=${action}
-            >
-              ${icon}<span class="label">${label}</span>
-            </editor-menu-action>
-          `
-        )}
-      </div>
-    </editor-menu-button>`;
-  }
-
-  private _moreButton() {
-    if (!this.block) return nothing;
-
-    const moreMenuActions = renderGroups(
-      this.moreGroups,
-      new SurfaceRefToolbarContext(this.block, new AbortController())
-    );
-
-    return html`<editor-menu-button
-      data-show
-      style=${styleMap({
-        '--content-padding': '8px',
-      })}
-      .button=${html`
-        <editor-icon-button .iconContainerPadding=${4} .iconSize=${'16px'}>
-          ${MoreVerticalIcon()}
-        </editor-icon-button>
-      `}
-      .popperOptions=${this._popoverOptions}
-    >
-      <div data-orientation="vertical">${moreMenuActions}</div>
-    </editor-menu-button>`;
-  }
-
-  private _renderButtons() {
-    if (!this.block) return nothing;
-    const readonly = this.block.model.doc.readonly;
-    const buttons = [
-      this._renderOpenButton(),
-      when(
-        !readonly,
-        () =>
-          html`<editor-icon-button
-            .iconContainerPadding=${4}
-            .iconSize=${'16px'}
-            @click=${() => {
-              if (!this.block) return;
-              this.std.command
-                .chain()
-                .pipe(draftSelectedModelsCommand, {
-                  selectedModels: [this.block.model],
-                })
-                .pipe(copySelectedModelsCommand)
-                .run();
-              toast(this.block.std.host, 'Copied to clipboard');
-            }}
-          >
-            ${CopyIcon()}
-          </editor-icon-button>`
-      ),
-      when(
-        !readonly,
-        () =>
-          html`<editor-icon-button
-            .iconContainerPadding=${4}
-            .iconSize=${'16px'}
-            @click=${() => {
-              if (!this.block) return;
-              this.block.captionElement.show();
-            }}
-          >
-            ${CaptionIcon()}
-          </editor-icon-button>`
-      ),
-      this._moreButton(),
-    ];
-
-    return buttons;
-  }
-
-  override render() {
-    if (!this.block) return nothing;
-    return html`${this._renderTitle()} ${this._renderButtons()}`;
-  }
-}
-
-declare global {
-  interface HTMLElementTagNameMap {
-    [AFFINE_SURFACE_REF_TOOLBAR]: AffineSurfaceRefToolbar;
-  }
-}
diff --git a/blocksuite/affine/blocks/block-surface-ref/src/widgets/utils.ts b/blocksuite/affine/blocks/block-surface-ref/src/widgets/utils.ts
deleted file mode 100644
index 7be9fe4edd571..0000000000000
--- a/blocksuite/affine/blocks/block-surface-ref/src/widgets/utils.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import type { CanvasRenderer } from '@blocksuite/affine-block-surface';
-import { ExportManager } from '@blocksuite/affine-block-surface';
-import type { SurfaceRefBlockComponent } from '@blocksuite/affine-block-surface-ref';
-import type { EditorHost } from '@blocksuite/block-std';
-import {
-  GfxControllerIdentifier,
-  type GfxModel,
-} from '@blocksuite/block-std/gfx';
-import { BlockSuiteError } from '@blocksuite/global/exceptions';
-import { Bound } from '@blocksuite/global/gfx';
-
-export const edgelessToBlob = async (
-  host: EditorHost,
-  options: {
-    surfaceRefBlock: SurfaceRefBlockComponent;
-    surfaceRenderer: CanvasRenderer;
-    edgelessElement: GfxModel;
-  }
-): Promise<Blob> => {
-  const { edgelessElement } = options;
-  const exportManager = host.std.get(ExportManager);
-  const bound = Bound.deserialize(edgelessElement.xywh);
-  const gfx = host.std.get(GfxControllerIdentifier);
-
-  const canvas = await exportManager.edgelessToCanvas(
-    options.surfaceRenderer,
-    bound,
-    gfx,
-    undefined,
-    undefined,
-    { zoom: options.surfaceRenderer.viewport.zoom }
-  );
-
-  if (!canvas) {
-    throw new BlockSuiteError(
-      BlockSuiteError.ErrorCode.ValueNotExists,
-      'Failed to export edgeless to canvas'
-    );
-  }
-
-  return new Promise((resolve, reject) => {
-    canvas.toBlob(blob => (blob ? resolve(blob) : reject(null)), 'image/png');
-  });
-};
-
-export const writeImageBlobToClipboard = async (blob: Blob) => {
-  await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]);
-};
diff --git a/blocksuite/affine/blocks/block-surface-ref/tsconfig.json b/blocksuite/affine/blocks/block-surface-ref/tsconfig.json
index 1f079a4057640..6472f3c17a1b0 100644
--- a/blocksuite/affine/blocks/block-surface-ref/tsconfig.json
+++ b/blocksuite/affine/blocks/block-surface-ref/tsconfig.json
@@ -10,6 +10,7 @@
     { "path": "../block-frame" },
     { "path": "../block-surface" },
     { "path": "../../components" },
+    { "path": "../../inlines/reference" },
     { "path": "../../model" },
     { "path": "../../shared" },
     { "path": "../../widgets/widget-slash-menu" },
diff --git a/blocksuite/affine/shared/src/services/toolbar-service/context.ts b/blocksuite/affine/shared/src/services/toolbar-service/context.ts
index bf64d18eb3bc4..b417342df90ff 100644
--- a/blocksuite/affine/shared/src/services/toolbar-service/context.ts
+++ b/blocksuite/affine/shared/src/services/toolbar-service/context.ts
@@ -1,5 +1,5 @@
 import {
-  type BlockComponent,
+  BlockComponent,
   BlockSelection,
   type BlockStdScope,
   SurfaceSelection,
@@ -177,17 +177,29 @@ abstract class ToolbarContextBase {
   }
 
   getCurrentBlockBy<T extends SelectionConstructor>(type: T) {
-    const selection = this.selection.find(type);
-    if (!selection) return null;
-    if (selection.is(SurfaceSelection)) {
-      const elementId = selection.elements[0];
-      const model = this.gfx.getElementById(elementId);
+    const getFromSelection = () => {
+      const selection = this.selection.find(type);
+      if (!selection) return null;
+      if (selection.is(SurfaceSelection)) {
+        const elementId = selection.elements[0];
+        const model = this.gfx.getElementById(elementId);
+        if (!model) return null;
+        return this.gfx.view.get(model.id) ?? null;
+      }
+      const model = this.store.getBlock(selection.blockId);
       if (!model) return null;
-      return this.gfx.view.get(model.id) ?? null;
-    }
-    const model = this.store.getBlock(selection.blockId);
-    if (!model) return null;
-    return this.view.getBlock(model.id);
+      return this.view.getBlock(model.id);
+    };
+
+    const getFromMessage = () => {
+      const msgEle = this.message$.peek()?.element;
+      if (msgEle instanceof BlockComponent) {
+        return msgEle;
+      }
+      return null;
+    };
+
+    return getFromSelection() ?? getFromMessage();
   }
 
   getCurrentBlock() {
@@ -211,13 +223,25 @@ abstract class ToolbarContextBase {
   }
 
   getCurrentModelBy<T extends SelectionConstructor>(type: T) {
-    const selection = this.selection.find(type);
-    if (!selection) return null;
-    if (selection.is(SurfaceSelection)) {
-      const elementId = selection.elements[0];
-      return elementId ? this.gfx.getElementById(elementId) : null;
-    }
-    return this.store.getBlock(selection.blockId)?.model ?? null;
+    const getFromSelection = () => {
+      const selection = this.selection.find(type);
+      if (!selection) return null;
+      if (selection.is(SurfaceSelection)) {
+        const elementId = selection.elements[0];
+        return elementId ? this.gfx.getElementById(elementId) : null;
+      }
+      return this.store.getBlock(selection.blockId)?.model ?? null;
+    };
+
+    const getFromMessage = () => {
+      const msgEle = this.message$.peek()?.element;
+      if (msgEle instanceof BlockComponent) {
+        return msgEle.model;
+      }
+      return null;
+    };
+
+    return getFromSelection() ?? getFromMessage();
   }
 
   getCurrentModel(): GfxModel | BlockModel | null {
diff --git a/packages/frontend/core/src/blocksuite/extensions/editor-config/toolbar/index.ts b/packages/frontend/core/src/blocksuite/extensions/editor-config/toolbar/index.ts
index c11779ebcce36..640846d79ae81 100644
--- a/packages/frontend/core/src/blocksuite/extensions/editor-config/toolbar/index.ts
+++ b/packages/frontend/core/src/blocksuite/extensions/editor-config/toolbar/index.ts
@@ -29,6 +29,7 @@ import {
   EmbedYoutubeBlockComponent,
   getDocContentWithMaxLength,
 } from '@blocksuite/affine/blocks/embed';
+import { SurfaceRefBlockComponent } from '@blocksuite/affine/blocks/surface-ref';
 import { toggleEmbedCardEditModal } from '@blocksuite/affine/components/embed-card-modal';
 import {
   notifyLinkedDocClearedAliases,
@@ -51,6 +52,7 @@ import {
   EmbedIframeBlockModel,
   EmbedLinkedDocModel,
   EmbedSyncedDocModel,
+  SurfaceRefBlockSchema,
 } from '@blocksuite/affine/model';
 import { getSelectedModelsCommand } from '@blocksuite/affine/shared/commands';
 import { ImageSelection } from '@blocksuite/affine/shared/selection';
@@ -70,6 +72,7 @@ import {
 import { matchModels } from '@blocksuite/affine/shared/utils';
 import type { ExtensionType } from '@blocksuite/affine/store';
 import {
+  ArrowDownSmallIcon,
   CopyAsImgaeIcon,
   CopyIcon,
   EditIcon,
@@ -481,7 +484,8 @@ function createOpenDocActions(
   target:
     | EmbedLinkedDocBlockComponent
     | EmbedSyncedDocBlockComponent
-    | AffineReference,
+    | AffineReference
+    | SurfaceRefBlockComponent,
   isSameDoc: boolean,
   actions = openDocActions.map(
     ({ type: mode, label, icon, enabled: when }, i) => ({
@@ -569,6 +573,131 @@ function createEdgelessOpenDocActionGroup(
   };
 }
 
+function createSurfaceRefToolbarConfig(baseUrl?: string): ToolbarModuleConfig {
+  return {
+    actions: [
+      {
+        id: 'b.open-surface-ref',
+        when: ctx =>
+          !!ctx.getCurrentBlockByType(SurfaceRefBlockComponent)?.referenceModel,
+        content: ctx => {
+          const surfaceRefBlock = ctx.getCurrentBlockByType(
+            SurfaceRefBlockComponent
+          );
+          if (!surfaceRefBlock) return null;
+
+          const actions = createOpenDocActions(ctx, surfaceRefBlock, false)
+            .map(action => ({
+              ...action,
+              ...action.generate(ctx),
+            }))
+            .map(action => {
+              if (action.id.endsWith('open-in-active-view')) {
+                action.label =
+                  I18n['com.affine.peek-view-controls.open-doc-in-edgeless']();
+              }
+              return action;
+            });
+          if (!actions.length) return null;
+
+          const styles = styleMap({
+            gap: 4,
+          });
+
+          return html`${keyed(
+            surfaceRefBlock,
+            html`<editor-menu-button
+              aria-label="Open"
+              .contentPadding=${'8px'}
+              .button=${html`<editor-icon-button
+                .iconSize=${'16px'}
+                .iconContainerPadding=${4}
+              >
+                ${OpenInNewIcon()} ${ArrowDownSmallIcon()}
+              </editor-icon-button>`}
+            >
+              <div data-orientation="vertical" style=${styles}>
+                ${repeat(
+                  actions,
+                  action => action.id,
+                  ({ label, icon, run, disabled }) => html`
+                    <editor-menu-action
+                      aria-label=${ifDefined(label)}
+                      ?disabled=${disabled}
+                      @click=${() => {
+                        run?.(ctx);
+                      }}
+                    >
+                      ${icon}<span class="label">${label}</span>
+                    </editor-menu-action>
+                  `
+                )}
+              </div>
+            </editor-menu-button>`
+          )}`;
+        },
+      },
+      {
+        id: 'a.clipboard',
+        placement: ActionPlacement.More,
+        actions: [
+          {
+            id: 'copy-link-to-surface-ref',
+            label: 'Copy original link',
+            icon: LinkIcon(),
+            when: ctx =>
+              !!ctx.getCurrentBlockByType(SurfaceRefBlockComponent)
+                ?.referenceModel,
+            run: ctx => {
+              const surfaceRefBlock = ctx.getCurrentBlockByType(
+                SurfaceRefBlockComponent
+              );
+              if (!surfaceRefBlock) return;
+
+              const refModel = surfaceRefBlock.referenceModel;
+              if (!refModel) return;
+
+              const { store, workspace, std } = ctx;
+              const pageId = store.doc.id;
+              const workspaceId = workspace.id;
+              const options: UseSharingUrl = {
+                workspaceId,
+                pageId,
+                mode: 'edgeless',
+              };
+
+              let type = '';
+              if (refModel instanceof GfxPrimitiveElementModel) {
+                options.elementIds = [refModel.id];
+                type = refModel.type;
+              } else if (refModel instanceof GfxBlockElementModel) {
+                options.blockIds = [refModel.id];
+                type = refModel.flavour;
+              }
+
+              const str = generateUrl({
+                ...options,
+                baseUrl: baseUrl ?? location.origin,
+              });
+              if (!str) return;
+
+              copyLinkToBlockStdScopeClipboard(str, std.clipboard)
+                .then(ok => {
+                  if (!ok) return;
+
+                  notify.success({ title: I18n['Copied link to clipboard']() });
+                })
+                .catch(console.error);
+
+              track.doc.editor.toolbar.copyBlockToLink({ type });
+            },
+          },
+        ],
+      },
+    ],
+  };
+}
+
 function renderOpenDocMenu(
   settings: EditorSettingExt,
   ctx: ToolbarContext,
@@ -877,11 +1006,11 @@ const embedIframeToolbarConfig = {
   },
   actions: [
     {
-      id: 'b.copy-link',
+      id: 'a.copy-link-and-edit',
       actions: [
         {
           id: 'copy-link',
-          tooltip: 'Copy original link',
+          tooltip: 'Copy link',
           icon: CopyIcon(),
           run(ctx) {
             const model = ctx.getCurrentBlockByType(
@@ -895,14 +1024,52 @@ const embedIframeToolbarConfig = {
             toast(ctx.host, 'Copied link to clipboard');
 
             ctx.track('CopiedLink', {
-              category: matchModels(model, [EmbedIframeBlockModel])
-                ? 'embed iframe block'
+              category: matchModels(model, [BookmarkBlockModel])
+                ? 'bookmark'
                 : 'link',
               type: 'card view',
               control: 'copy link',
             });
           },
         },
+        {
+          id: 'edit',
+          tooltip: 'Edit',
+          icon: EditIcon(),
+          run(ctx) {
+            const component = ctx.getCurrentBlockByType(
+              EmbedIframeBlockComponent
+            );
+            if (!component) return;
+
+            ctx.hide();
+
+            const model = component.model;
+            const abortController = new AbortController();
+            abortController.signal.onabort = () => ctx.show();
+
+            toggleEmbedCardEditModal(
+              ctx.host,
+              model,
+              'card',
+              undefined,
+              undefined,
+              (_std, _component, props) => {
+                ctx.store.updateBlock(model, props);
+                component.requestUpdate();
+              },
+              abortController
+            );
+
+            ctx.track('OpenedAliasPopup', {
+              category: matchModels(model, [BookmarkBlockModel])
+                ? 'bookmark'
+                : 'link',
+              type: 'card view',
+              control: 'edit',
+            });
+          },
+        },
       ],
     },
   ],
@@ -1052,5 +1219,12 @@ export const createCustomToolbarExtension = (
         when: ctx => ctx.getSurfaceModels().length === 1,
       },
     }),
+
+    ToolbarModuleExtension({
+      id: BlockFlavourIdentifier(
+        `custom:${SurfaceRefBlockSchema.model.flavour}`
+      ),
+      config: createSurfaceRefToolbarConfig(baseUrl),
+    }),
   ];
 };
diff --git a/packages/frontend/core/src/blocksuite/extensions/entry/enable-mobile.ts b/packages/frontend/core/src/blocksuite/extensions/entry/enable-mobile.ts
index 09053022012d1..36d9799c1cf18 100644
--- a/packages/frontend/core/src/blocksuite/extensions/entry/enable-mobile.ts
+++ b/packages/frontend/core/src/blocksuite/extensions/entry/enable-mobile.ts
@@ -10,7 +10,6 @@ import {
 } from '@blocksuite/affine/blocks/code';
 import { imageToolbarWidget } from '@blocksuite/affine/blocks/image';
 import { ParagraphBlockConfigExtension } from '@blocksuite/affine/blocks/paragraph';
-import { surfaceRefToolbarWidget } from '@blocksuite/affine/blocks/surface-ref';
 import type {
   Container,
   ServiceIdentifier,
@@ -136,7 +135,6 @@ export function enableMobileExtension(
 ): void {
   specBuilder.omit(codeToolbarWidget);
   specBuilder.omit(imageToolbarWidget);
-  specBuilder.omit(surfaceRefToolbarWidget);
   specBuilder.omit(toolbarWidget);
   specBuilder.omit(SlashMenuExtension);
   specBuilder.extend([
diff --git a/packages/frontend/core/src/desktop/pages/workspace/detail-page/detail-page.tsx b/packages/frontend/core/src/desktop/pages/workspace/detail-page/detail-page.tsx
index 399b614aea022..79248d11bba42 100644
--- a/packages/frontend/core/src/desktop/pages/workspace/detail-page/detail-page.tsx
+++ b/packages/frontend/core/src/desktop/pages/workspace/detail-page/detail-page.tsx
@@ -204,6 +204,7 @@ const DetailPageImpl = memo(function DetailPageImpl() {
                   workbench.openDoc(
                     {
                       docId: pageId,
+                      mode: params?.mode,
                       blockIds: params?.blockIds,
                       elementIds: params?.elementIds,
                     },
diff --git a/packages/frontend/i18n/src/i18n.gen.ts b/packages/frontend/i18n/src/i18n.gen.ts
index cfce617cf3e34..51be910a899e6 100644
--- a/packages/frontend/i18n/src/i18n.gen.ts
+++ b/packages/frontend/i18n/src/i18n.gen.ts
@@ -4351,6 +4351,10 @@ export function useAFFiNEI18N(): {
       * `Open this doc`
       */
     ["com.affine.peek-view-controls.open-doc"](): string;
+    /**
+      * `Open in edgeless`
+      */
+    ["com.affine.peek-view-controls.open-doc-in-edgeless"](): string;
     /**
       * `Open in new tab`
       */
diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json
index 66dcaa5b99003..3783463017ca5 100644
--- a/packages/frontend/i18n/src/resources/en.json
+++ b/packages/frontend/i18n/src/resources/en.json
@@ -1079,6 +1079,7 @@
   "com.affine.payment.license-success.copy": "Copied key to clipboard",
   "com.affine.peek-view-controls.close": "Close",
   "com.affine.peek-view-controls.open-doc": "Open this doc",
+  "com.affine.peek-view-controls.open-doc-in-edgeless": "Open in edgeless",
   "com.affine.peek-view-controls.open-doc-in-new-tab": "Open in new tab",
   "com.affine.peek-view-controls.open-doc-in-split-view": "Open in split view",
   "com.affine.peek-view-controls.open-info": "Open doc info",
diff --git a/tests/affine-local/e2e/peek-view.spec.ts b/tests/affine-local/e2e/peek-view.spec.ts
index 5888efb6712dc..4124a5f3a0549 100644
--- a/tests/affine-local/e2e/peek-view.spec.ts
+++ b/tests/affine-local/e2e/peek-view.spec.ts
@@ -134,11 +134,11 @@ test('can open peek view for embedded frames', async ({ page }) => {
   await expect(surfaceRef).toBeVisible();
   await surfaceRef.hover();
   await page
-    .locator('affine-surface-ref-toolbar editor-menu-button[aria-label="Open"]')
+    .locator('affine-toolbar-widget editor-menu-button[aria-label="Open"]')
     .click();
   await page
     .locator(
-      'affine-surface-ref-toolbar editor-menu-action[aria-label="Open in center peek"]'
+      'affine-toolbar-widget editor-menu-action[aria-label="Open in center peek"]'
     )
     .click();
 
diff --git a/tools/utils/src/workspace.gen.ts b/tools/utils/src/workspace.gen.ts
index d56a190407892..df89a694c0887 100644
--- a/tools/utils/src/workspace.gen.ts
+++ b/tools/utils/src/workspace.gen.ts
@@ -361,6 +361,7 @@ export const PackageList = [
       'blocksuite/affine/blocks/block-frame',
       'blocksuite/affine/blocks/block-surface',
       'blocksuite/affine/components',
+      'blocksuite/affine/inlines/reference',
       'blocksuite/affine/model',
       'blocksuite/affine/shared',
       'blocksuite/affine/widgets/widget-slash-menu',
diff --git a/yarn.lock b/yarn.lock
index ec24728f06ec0..0c3b060b1ebc9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2792,6 +2792,7 @@ __metadata:
     "@blocksuite/affine-block-frame": "workspace:*"
     "@blocksuite/affine-block-surface": "workspace:*"
     "@blocksuite/affine-components": "workspace:*"
+    "@blocksuite/affine-inline-reference": "workspace:*"
     "@blocksuite/affine-model": "workspace:*"
     "@blocksuite/affine-shared": "workspace:*"
     "@blocksuite/affine-widget-slash-menu": "workspace:*"


Reviews:
user: login: fundon
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-27T11:17:44Z
user: login: fundon
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-27T11:53:16Z
user: login: fundon
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-28T01:32:57Z
user: login: fundon
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-28T02:02:56Z
user: login: fundon
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-28T02:07:40Z
user: login: L-Sun
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T03:22:26Z
user: login: fundon
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-28T03:48:33Z
user: login: fundon
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-28T03:57:12Z
user: login: fundon
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-28T04:47:41Z

Review comments:
diff_hunk: @@ -0,0 +1,123 @@
+import { toast } from '@blocksuite/affine-components/toast';
+import {
+  copySelectedModelsCommand,
+  draftSelectedModelsCommand,
+} from '@blocksuite/affine-shared/commands';
+import {
+  ActionPlacement,
+  type ToolbarModuleConfig,
+} from '@blocksuite/affine-shared/services';
+import { CaptionIcon, CopyIcon, DeleteIcon } from '@blocksuite/icons/lit';
+import { html, nothing } from 'lit';
+
+import { SurfaceRefBlockComponent } from '../surface-ref-block';
+import { surfaceRefToBlob, writeImageBlobToClipboard } from '../utils';
+
+export const surfaceRefToolbarModuleConfig: ToolbarModuleConfig = {
+  actions: [
+    {
+      id: 'a.surface-ref-title',
+      content: ctx => {
+        const surfaceRefBlock =
+          ctx.message$.peek()?.element ?? ctx.getCurrentBlock();
+        if (!(surfaceRefBlock instanceof SurfaceRefBlockComponent))
+          return html`${nothing}`;
path: blocksuite/affine/blocks/block-surface-ref/src/configs/toolbar.ts
user: login: fundon
body: return `null` directly
created_at: 2025-03-27T11:17:43Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,126 @@
+import { toast } from '@blocksuite/affine-components/toast';
+import {
+  copySelectedModelsCommand,
+  draftSelectedModelsCommand,
+} from '@blocksuite/affine-shared/commands';
+import {
+  ActionPlacement,
+  type ToolbarModuleConfig,
+} from '@blocksuite/affine-shared/services';
+import { CaptionIcon, CopyIcon, DeleteIcon } from '@blocksuite/icons/lit';
+import { html } from 'lit';
+
+import { SurfaceRefBlockComponent } from '../surface-ref-block';
+import { surfaceRefToBlob, writeImageBlobToClipboard } from '../utils';
+
+export const surfaceRefToolbarModuleConfig: ToolbarModuleConfig = {
+  actions: [
+    {
+      id: 'a.surface-ref-title',
+      content: ctx => {
+        const surfaceRefBlock = ctx.getCurrentBlockByType(
+          SurfaceRefBlockComponent
+        );
+        if (!surfaceRefBlock) return null;
+
+        return html`<surface-ref-toolbar-title
+          .referenceModel=${surfaceRefBlock.referenceModel}
+        ></surface-ref-toolbar-title>`;
+      },
+    },
+    {
+      id: 'c.copy-surface-ref',
+      label: 'Copy',
+      icon: CopyIcon(),
+      when: ({ readonly }) => !readonly,
+      run: ctx => {
+        const surfaceRefBlock = ctx.getCurrentBlockByType(
+          SurfaceRefBlockComponent
+        );
+        if (!surfaceRefBlock) return;
+
+        ctx.chain
+          .pipe(draftSelectedModelsCommand, {
+            selectedModels: [surfaceRefBlock.model],
+          })
+          .pipe(copySelectedModelsCommand)
+          .run();
+
+        toast(surfaceRefBlock.std.host, 'Copied to clipboard');
+      },
+    },
+    {
+      id: 'd.surface-ref-caption',
+      icon: CaptionIcon(),
+      when: ({ readonly }) => !readonly,
+      run: ctx => {
+        const surfaceRefBlock = ctx.getCurrentBlockByType(
+          SurfaceRefBlockComponent
+        );
+        if (!surfaceRefBlock) return;
+
+        surfaceRefBlock.captionElement.show();
+      },
+    },
+    {
+      id: 'a.clipboard',
+      placement: ActionPlacement.More,
+      when: ctx => {
+        const surfaceRefBlock = ctx.getCurrentBlock();
+        if (!(surfaceRefBlock instanceof SurfaceRefBlockComponent))
+          return false;
+
+        return !!surfaceRefBlock.referenceModel;
+      },
+      actions: [
+        {
+          id: 'a.surface-ref-copy-as-image',
+          label: 'Copy as Image',
+          icon: CopyIcon(),
+          placement: ActionPlacement.More,
+          when: ctx => {
+            const surfaceRefBlock = ctx.getCurrentBlockByType(
+              SurfaceRefBlockComponent
+            );
+            if (!surfaceRefBlock) return false;
+
+            return !!surfaceRefBlock.referenceModel;
+          },
+          run: ctx => {
+            const surfaceRefBlock = ctx.getCurrentBlockByType(
+              SurfaceRefBlockComponent
+            );
+            if (!surfaceRefBlock) return;
+
+            surfaceRefToBlob(surfaceRefBlock)
+              .then(async blob => {
+                if (!blob) {
+                  toast(ctx.host, 'Failed to render surface-ref to image');
+                } else {
+                  await writeImageBlobToClipboard(blob);
+                  toast(ctx.host, 'Copied image to clipboard');
+                }
+              })
+              .catch(console.error);
+          },
+        },
+        // TODO(@L-Sun): add duplicate action after refactoring root-block/edgeless
+      ],
+    },
+    {
+      id: 'g.surface-ref-deletion',
+      label: 'Delete',
+      icon: DeleteIcon(),
+      placement: ActionPlacement.More,
path: blocksuite/affine/blocks/block-surface-ref/src/configs/toolbar.ts
user: login: fundon
body: add `variant: 'destructive'` for highlighting text color
created_at: 2025-03-27T11:53:16Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,68 @@
+import {
+  FrameBlockModel,
+  GroupElementModel,
+  MindmapElementModel,
+  ShapeElementModel,
+} from '@blocksuite/affine-model';
+import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
+import { ShadowlessElement } from '@blocksuite/block-std';
+import type { GfxModel } from '@blocksuite/block-std/gfx';
+import {
+  EdgelessIcon,
+  FrameIcon,
+  GroupIcon,
+  MindmapIcon,
+} from '@blocksuite/icons/lit';
+import { css, html, type TemplateResult } from 'lit';
+import { property } from 'lit/decorators.js';
+
+export class SurfaceRefToolbarTitle extends ShadowlessElement {
+  static override styles = css`
+    surface-ref-toolbar-title {
+      display: flex;
+      padding: 2px 4px;
+      margin-right: auto;
+      align-items: center;
+      gap: 4px;
+      border-radius: 2px;
+      background: ${unsafeCSSVarV2('button/iconButtonSolid')};
+
+      svg {
+        color: ${unsafeCSSVarV2('icon/primary')};
+        width: 16px;
+        height: 16px;
+      }
+
+      span {
+        color: ${unsafeCSSVarV2('text/primary')};
+        font-size: 12px;
+        font-weight: 500;
+        line-height: 20px;
+      }
+    }
+  `;
+
+  @property({ attribute: false })
+  accessor referenceModel: GfxModel | null = null;
path: blocksuite/affine/blocks/block-surface-ref/src/components/surface-ref-toolbar-title.ts
user: login: fundon
body: replace it with `title` and `icon`
created_at: 2025-03-28T01:32:57Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,126 @@
+import { toast } from '@blocksuite/affine-components/toast';
+import {
+  copySelectedModelsCommand,
+  draftSelectedModelsCommand,
+} from '@blocksuite/affine-shared/commands';
+import {
+  ActionPlacement,
+  type ToolbarModuleConfig,
+} from '@blocksuite/affine-shared/services';
+import { CaptionIcon, CopyIcon, DeleteIcon } from '@blocksuite/icons/lit';
+import { html } from 'lit';
+
+import { SurfaceRefBlockComponent } from '../surface-ref-block';
+import { surfaceRefToBlob, writeImageBlobToClipboard } from '../utils';
+
+export const surfaceRefToolbarModuleConfig: ToolbarModuleConfig = {
+  actions: [
+    {
+      id: 'a.surface-ref-title',
+      content: ctx => {
+        const surfaceRefBlock = ctx.getCurrentBlockByType(
+          SurfaceRefBlockComponent
+        );
+        if (!surfaceRefBlock) return null;
+
+        return html`<surface-ref-toolbar-title
+          .referenceModel=${surfaceRefBlock.referenceModel}
+        ></surface-ref-toolbar-title>`;
+      },
+    },
+    {
+      id: 'c.copy-surface-ref',
+      label: 'Copy',
+      icon: CopyIcon(),
+      when: ({ readonly }) => !readonly,
path: blocksuite/affine/blocks/block-surface-ref/src/configs/toolbar.ts
user: login: fundon
body: unnecessary
created_at: 2025-03-28T02:02:56Z
author_association: COLLABORATOR
diff_hunk: @@ -1090,5 +1216,12 @@ export const createCustomToolbarExtension = (
         when: ctx => ctx.getSurfaceModels().length === 1,
       },
     }),
+
+    ToolbarModuleExtension({
+      id: BlockFlavourIdentifier(
+        `custom:${SurfaceRefBlockSchema.model.flavour}`
+      ),
+      config: createSurfaceRefToolbarConfig(baseUrl),
+    }),
path: packages/frontend/core/src/blocksuite/extensions/editor-config/toolbar/index.ts
user: login: fundon
body: add `when: ctx => ctx.getSurfaceModels().length === 1,`, show only if only one
created_at: 2025-03-28T02:07:39Z
author_association: COLLABORATOR
diff_hunk: @@ -1090,5 +1216,12 @@ export const createCustomToolbarExtension = (
         when: ctx => ctx.getSurfaceModels().length === 1,
       },
     }),
+
+    ToolbarModuleExtension({
+      id: BlockFlavourIdentifier(
+        `custom:${SurfaceRefBlockSchema.model.flavour}`
+      ),
+      config: createSurfaceRefToolbarConfig(baseUrl),
+    }),
path: packages/frontend/core/src/blocksuite/extensions/editor-config/toolbar/index.ts
user: login: L-Sun
body: I think the `surface-ref-block` is only rendered in page mode, so this constraint is unnecessary
created_at: 2025-03-28T03:22:26Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,125 @@
+import { toast } from '@blocksuite/affine-components/toast';
+import {
+  copySelectedModelsCommand,
+  draftSelectedModelsCommand,
+} from '@blocksuite/affine-shared/commands';
+import {
+  ActionPlacement,
+  type ToolbarModuleConfig,
+} from '@blocksuite/affine-shared/services';
+import { CaptionIcon, CopyIcon, DeleteIcon } from '@blocksuite/icons/lit';
+import { html } from 'lit';
+
+import { SurfaceRefBlockComponent } from '../surface-ref-block';
+import { surfaceRefToBlob, writeImageBlobToClipboard } from '../utils';
+
+export const surfaceRefToolbarModuleConfig: ToolbarModuleConfig = {
+  actions: [
+    {
+      id: 'a.surface-ref-title',
+      content: ctx => {
+        const surfaceRefBlock = ctx.getCurrentBlockByType(
+          SurfaceRefBlockComponent
+        );
+        if (!surfaceRefBlock) return null;
+
+        return html`<surface-ref-toolbar-title
path: blocksuite/affine/blocks/block-surface-ref/src/configs/toolbar.ts
user: login: fundon
body: use `keyed(block, html``)`  wrap it
created_at: 2025-03-28T03:57:12Z
author_association: COLLABORATOR

PR comments:
user: login: L-Sun
created_at: 2025-03-27T11:08:10Z
body: * **#11246** <a href="https://app.graphite.dev/github/pr/toeverything/AFFiNE/11246?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a> 👈 <a href="https://app.graphite.dev/github/pr/toeverything/AFFiNE/11246?utm_source=stack-comment-view-in-graphite" target="_blank">(View in Graphite)</a>
* **#11243** <a href="https://app.graphite.dev/github/pr/toeverything/AFFiNE/11243?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a>
* `canary`
<hr />

## How to use the Graphite Merge Queue

Add either label to this PR to merge it via the merge queue:
- <kbd>merge</kbd> - adds this PR to the back of the merge queue
- <kbd>hotfix</kbd> - for urgent hot fixes, skip the queue and merge this PR next

You must have a Graphite account in order to use the merge queue. Sign up using [this link](https://app.graphite.dev/invite/github/toeverything?ref=merge-queue-instructions-in-stack-comment&prId=11246).

<h2></h2>
<sub>

An organization admin has enabled the [Graphite Merge Queue](https://graphite.dev/docs/graphite-merge-queue) in this repository.

</sub>

<sub>

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

</sub>

<h2></h2>

This stack of pull requests is managed by <a href="https://graphite.dev?utm-source=stack-comment"><b>Graphite</b></a>. Learn more about <a href="https://stacking.dev/?utm_source=stack-comment">stacking</a>.
<!-- Current dependencies on/for this PR: -->
author_association: MEMBER
user: login: codecov[bot]
created_at: 2025-03-27T11:16:08Z
body: ## [Codecov](https://app.codecov.io/gh/toeverything/AFFiNE/pull/11246?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything) Report
Attention: Patch coverage is `34.90566%` with `69 lines` in your changes missing coverage. Please review.
> Project coverage is 54.15%. Comparing base [(`69f393f`)](https://app.codecov.io/gh/toeverything/AFFiNE/commit/69f393fe2f19d41f483febe75b62a9efe4c3fd32?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything) to head [(`af91a02`)](https://app.codecov.io/gh/toeverything/AFFiNE/commit/af91a0217f485e8d3794c3f866f0d533b6b86ceb?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything).
> Report is 1 commits behind head on canary.

| [Files with missing lines](https://app.codecov.io/gh/toeverything/AFFiNE/pull/11246?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything) | Patch % | Lines |
|---|---|---|
| [...ne/blocks/block-surface-ref/src/configs/toolbar.ts](https://app.codecov.io/gh/toeverything/AFFiNE/pull/11246?src=pr&el=tree&filepath=blocksuite%2Faffine%2Fblocks%2Fblock-surface-ref%2Fsrc%2Fconfigs%2Ftoolbar.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything#diff-YmxvY2tzdWl0ZS9hZmZpbmUvYmxvY2tzL2Jsb2NrLXN1cmZhY2UtcmVmL3NyYy9jb25maWdzL3Rvb2xiYXIudHM=) | 28.57% | [20 Missing :warning: ](https://app.codecov.io/gh/toeverything/AFFiNE/pull/11246?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything) |
| [...ce-ref/src/components/surface-ref-toolbar-title.ts](https://app.codecov.io/gh/toeverything/AFFiNE/pull/11246?src=pr&el=tree&filepath=blocksuite%2Faffine%2Fblocks%2Fblock-surface-ref%2Fsrc%2Fcomponents%2Fsurface-ref-toolbar-title.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything#diff-YmxvY2tzdWl0ZS9hZmZpbmUvYmxvY2tzL2Jsb2NrLXN1cmZhY2UtcmVmL3NyYy9jb21wb25lbnRzL3N1cmZhY2UtcmVmLXRvb2xiYXItdGl0bGUudHM=) | 11.76% | [15 Missing :warning: ](https://app.codecov.io/gh/toeverything/AFFiNE/pull/11246?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything) |
| [...ine/shared/src/services/toolbar-service/context.ts](https://app.codecov.io/gh/toeverything/AFFiNE/pull/11246?src=pr&el=tree&filepath=blocksuite%2Faffine%2Fshared%2Fsrc%2Fservices%2Ftoolbar-service%2Fcontext.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything#diff-YmxvY2tzdWl0ZS9hZmZpbmUvc2hhcmVkL3NyYy9zZXJ2aWNlcy90b29sYmFyLXNlcnZpY2UvY29udGV4dC50cw==) | 48.27% | [15 Missing :warning: ](https://app.codecov.io/gh/toeverything/AFFiNE/pull/11246?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything) |
| [...suite/affine/blocks/block-surface-ref/src/utils.ts](https://app.codecov.io/gh/toeverything/AFFiNE/pull/11246?src=pr&el=tree&filepath=blocksuite%2Faffine%2Fblocks%2Fblock-surface-ref%2Fsrc%2Futils.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything#diff-YmxvY2tzdWl0ZS9hZmZpbmUvYmxvY2tzL2Jsb2NrLXN1cmZhY2UtcmVmL3NyYy91dGlscy50cw==) | 17.64% | [14 Missing :warning: ](https://app.codecov.io/gh/toeverything/AFFiNE/pull/11246?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything) |
| [.../blocks/block-surface-ref/src/surface-ref-block.ts](https://app.codecov.io/gh/toeverything/AFFiNE/pull/11246?src=pr&el=tree&filepath=blocksuite%2Faffine%2Fblocks%2Fblock-surface-ref%2Fsrc%2Fsurface-ref-block.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything#diff-YmxvY2tzdWl0ZS9hZmZpbmUvYmxvY2tzL2Jsb2NrLXN1cmZhY2UtcmVmL3NyYy9zdXJmYWNlLXJlZi1ibG9jay50cw==) | 64.28% | [5 Missing :warning: ](https://app.codecov.io/gh/toeverything/AFFiNE/pull/11246?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           canary   #11246      +/-   ##
==========================================
- Coverage   54.19%   54.15%   -0.04%     
==========================================
  Files        2559     2557       -2     
  Lines      115389   115362      -27     
  Branches    18810    18809       -1     
==========================================
- Hits        62530    62475      -55     
- Misses      51513    51557      +44     
+ Partials     1346     1330      -16     
```

| [Flag](https://app.codecov.io/gh/toeverything/AFFiNE/pull/11246/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything) | Coverage Δ | |
|---|---|---|
| [server-test](https://app.codecov.io/gh/toeverything/AFFiNE/pull/11246/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything) | `78.65% <ø> (ø)` | |
| [unittest](https://app.codecov.io/gh/toeverything/AFFiNE/pull/11246/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything) | `30.21% <16.98%> (-0.09%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/toeverything/AFFiNE/pull/11246?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: graphite-app[bot]
created_at: 2025-03-28T05:47:44Z
body: ### Merge activity

* **Mar 28, 1:47 AM EDT**: `L-Sun` added this pull request to the [Graphite merge queue](https://app.graphite.dev/merges?org=toeverything&repo=AFFiNE).
* **Mar 28, 2:00 AM EDT**: A user  merged this pull request with the [Graphite merge queue](https://app.graphite.dev/merges?org=toeverything&repo=AFFiNE).

author_association: CONTRIBUTOR