@@ -143,6 +194,7 @@ export const ResourceContent = memo(function ResourceContent({
canEdit={false}
previewMode={previewMode ?? 'preview'}
streamingContent={textStreamingContent}
+ isAgentEditing={isAgentEditing}
disableStreamingAutoScroll={disableStreamingAutoScroll}
previewContextKey={previewContextKey}
/>
@@ -165,6 +217,7 @@ export const ResourceContent = memo(function ResourceContent({
streamingContent={
previewSession?.fileId === resource.id ? textStreamingContent : undefined
}
+ isAgentEditing={isAgentEditing}
disableStreamingAutoScroll={disableStreamingAutoScroll}
previewContextKey={previewContextKey}
/>
@@ -550,6 +603,7 @@ interface EmbeddedFileProps {
filePath?: string
previewMode?: PreviewMode
streamingContent?: string
+ isAgentEditing?: boolean
disableStreamingAutoScroll?: boolean
previewContextKey?: string
}
@@ -560,6 +614,7 @@ function EmbeddedFile({
filePath,
previewMode,
streamingContent,
+ isAgentEditing,
disableStreamingAutoScroll = false,
previewContextKey,
}: EmbeddedFileProps) {
@@ -601,6 +656,7 @@ function EmbeddedFile({
canEdit={canEdit}
previewMode={previewMode}
streamingContent={streamingContent}
+ isAgentEditing={isAgentEditing}
disableStreamingAutoScroll={disableStreamingAutoScroll}
previewContextKey={previewContextKey}
/>
diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/mothership-view.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/mothership-view.tsx
index 6cb035617c2..e65c96e96df 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/mothership-view.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/mothership-view.tsx
@@ -52,6 +52,7 @@ interface MothershipViewProps {
isCollapsed: boolean
className?: string
previewSession?: FilePreviewSession | null
+ isAgentResponding?: boolean
genericResourceData?: GenericResourceData
}
@@ -65,6 +66,7 @@ export const MothershipView = memo(
isCollapsed,
className,
previewSession,
+ isAgentResponding,
genericResourceData,
}: MothershipViewProps,
ref
@@ -136,6 +138,7 @@ export const MothershipView = memo(
resource={active}
previewMode={isActivePreviewable ? previewMode : undefined}
previewSession={previewForActive}
+ isAgentResponding={isAgentResponding}
genericResourceData={active.type === 'generic' ? genericResourceData : undefined}
previewContextKey={chatId}
onNotFound={(resourceId) => removeResource('log', resourceId)}
diff --git a/apps/sim/app/workspace/[workspaceId]/home/home.tsx b/apps/sim/app/workspace/[workspaceId]/home/home.tsx
index b22b783d7e3..4e21e754eb0 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/home.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/home/home.tsx
@@ -461,6 +461,7 @@ export function Home({ chatId, userName, userId, initialResourceId = null }: Hom
activeResourceId={activeResourceId}
isCollapsed={isResourceCollapsed}
previewSession={previewSession}
+ isAgentResponding={isSending}
genericResourceData={genericResourceData ?? undefined}
className={skipResourceTransition ? '!transition-none' : undefined}
/>
diff --git a/apps/sim/app/workspace/[workspaceId]/providers/global-commands-provider.test.tsx b/apps/sim/app/workspace/[workspaceId]/providers/global-commands-provider.test.tsx
new file mode 100644
index 00000000000..84dfd588e18
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/providers/global-commands-provider.test.tsx
@@ -0,0 +1,89 @@
+/**
+ * @vitest-environment jsdom
+ */
+import { act, type ReactNode } from 'react'
+import { createRoot, type Root } from 'react-dom/client'
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+
+const { mockIsMac } = vi.hoisted(() => ({ mockIsMac: vi.fn(() => false) }))
+vi.mock('@/lib/core/utils/platform', () => ({ isMacPlatform: mockIsMac }))
+vi.mock('next/navigation', () => ({ useRouter: () => ({ push: vi.fn() }) }))
+
+import {
+ GlobalCommandsProvider,
+ useRegisterGlobalCommands,
+} from '@/app/workspace/[workspaceId]/providers/global-commands-provider'
+
+function RegisterModK({ handler }: { handler: () => void }) {
+ useRegisterGlobalCommands([{ id: 'search', shortcut: 'Mod+K', handler }])
+ return null
+}
+
+let container: HTMLDivElement
+let root: Root
+
+function mount(ui: ReactNode) {
+ act(() => {
+ root.render(ui)
+ })
+}
+
+/** Non-mac (mocked): `Mod` resolves to Ctrl, so Ctrl+K matches a `Mod+K` shortcut. */
+function pressModK() {
+ window.dispatchEvent(
+ new KeyboardEvent('keydown', { key: 'k', ctrlKey: true, bubbles: true, cancelable: true })
+ )
+}
+
+beforeEach(() => {
+ container = document.createElement('div')
+ document.body.appendChild(container)
+ root = createRoot(container)
+})
+
+afterEach(() => {
+ act(() => root.unmount())
+ container.remove()
+ vi.clearAllMocks()
+})
+
+describe('GlobalCommandsProvider owned-shortcut yielding', () => {
+ it('fires a global command when nothing owns the shortcut', () => {
+ const handler = vi.fn()
+ mount(
+
+
+
+ )
+ pressModK()
+ expect(handler).toHaveBeenCalledTimes(1)
+ })
+
+ it('yields the shortcut to a focused element that declares it owns it', () => {
+ const handler = vi.fn()
+ mount(
+
+
+ {/* biome-ignore lint/a11y/noNoninteractiveTabindex: focusable stand-in for the editor */}
+
+
+ )
+ ;(container.querySelector('[data-owned-shortcuts]') as HTMLElement).focus()
+ pressModK()
+ expect(handler).not.toHaveBeenCalled()
+ })
+
+ it('still fires when the focused element owns only a different shortcut', () => {
+ const handler = vi.fn()
+ mount(
+
+
+ {/* biome-ignore lint/a11y/noNoninteractiveTabindex: focusable stand-in for the editor */}
+
+
+ )
+ ;(container.querySelector('[data-owned-shortcuts]') as HTMLElement).focus()
+ pressModK()
+ expect(handler).toHaveBeenCalledTimes(1)
+ })
+})
diff --git a/apps/sim/app/workspace/[workspaceId]/providers/global-commands-provider.tsx b/apps/sim/app/workspace/[workspaceId]/providers/global-commands-provider.tsx
index 3c3d6ae0db6..ee8ae3f5c97 100644
--- a/apps/sim/app/workspace/[workspaceId]/providers/global-commands-provider.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/providers/global-commands-provider.tsx
@@ -86,6 +86,30 @@ function matchesShortcut(e: KeyboardEvent, parsed: ParsedShortcut): boolean {
)
}
+/** Platform-resolved signature of a shortcut, so `Mod+K`, `Cmd+K`, and `Meta+K` compare equal on mac. */
+function shortcutSignature(parsed: ParsedShortcut, isMac: boolean): string {
+ const ctrl = parsed.ctrl || (parsed.mod ? !isMac : false)
+ const meta = parsed.meta || (parsed.mod ? isMac : false)
+ return `${parsed.key}|${+ctrl}|${+meta}|${+!!parsed.shift}|${+!!parsed.alt}`
+}
+
+/**
+ * Whether the focused element (or an ancestor) declares it owns `parsed` via a comma-separated
+ * `data-owned-shortcuts` attribute (e.g. a rich-text editor that binds `Mod+K` to links). Such a
+ * shortcut is left for that element to handle instead of firing the global command.
+ */
+function focusedElementOwnsShortcut(parsed: ParsedShortcut, isMac: boolean): boolean {
+ const active = document.activeElement
+ const owner = active instanceof HTMLElement ? active.closest('[data-owned-shortcuts]') : null
+ if (!owner) return false
+ const target = shortcutSignature(parsed, isMac)
+ return (owner.getAttribute('data-owned-shortcuts') ?? '')
+ .split(',')
+ .map((entry) => entry.trim())
+ .filter(Boolean)
+ .some((entry) => shortcutSignature(parseShortcut(entry), isMac) === target)
+}
+
export function GlobalCommandsProvider({ children }: { children: ReactNode }) {
const registryRef = useRef