Skip to content

Commit d387c6f

Browse files
committed
fix(extension): preserve composer focus and pending page reads
1 parent 1990323 commit d387c6f

8 files changed

Lines changed: 385 additions & 20 deletions

File tree

‎apps/sim/app/extension/hooks/use-page-context.ts‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,14 @@ export function usePageContext(origins: string[], scopeKey: string) {
7676
let focusRequested = false
7777
const confirmFocus = () => {
7878
const bridge = bridgeRef.current
79-
if (!focusRequested || bridge?.sessionId !== sessionId || !document.hasFocus()) return
79+
if (
80+
!focusRequested ||
81+
bridge?.sessionId !== sessionId ||
82+
!document.hasFocus() ||
83+
!composerFocus?.current?.isFocused()
84+
) {
85+
return
86+
}
8087
focusRequested = false
8188
const message: AppToExtensionMessage = {
8289
channel: 'sim-extension',
@@ -98,7 +105,7 @@ export function usePageContext(origins: string[], scopeKey: string) {
98105
if (parsed.data.type === 'focus-composer') {
99106
if (bridgeRef.current?.sessionId === sessionId) {
100107
focusRequested = true
101-
composerFocus?.current?.()
108+
composerFocus?.current?.focus()
102109
confirmFocus()
103110
}
104111
return

‎apps/sim/app/extension/providers.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { useSession } from '@/lib/auth/client'
77
import ExtensionLoading from '@/app/extension/loading'
88
import { ExtensionSignIn } from '@/app/extension/sign-in'
99
import { FeatureFlagsProvider } from '@/app/workspace/[workspaceId]/providers/feature-flags-provider'
10-
import { ChatInputFocusContext } from '@/hooks/use-chat-input-focus'
10+
import { ChatInputFocusContext, type ChatInputFocusHandle } from '@/hooks/use-chat-input-focus'
1111

1212
interface ExtensionIdentity {
1313
origins: string[]
@@ -29,7 +29,7 @@ export function ExtensionProviders({
2929
modelSelectorEnabled,
3030
planModeEnabled,
3131
}: ExtensionProvidersProps) {
32-
const focusComposer = useRef<(() => void) | null>(null)
32+
const focusComposer = useRef<ChatInputFocusHandle | null>(null)
3333
const { data: session, isPending, error } = useSession()
3434
if (isPending) return <ExtensionLoading />
3535
const accessDenied = isApiClientError(error) && (error.status === 401 || error.status === 403)

‎apps/sim/app/o/[organizationId]/components/organization-conversation.tsx‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ import { useFeatureFlag } from '@/app/workspace/[workspaceId]/providers/feature-
6767
import { useFileAttachments } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-file-attachments'
6868
import { mentionifyIntegrations } from '@/blocks/integration-matcher'
6969
import { useMarkMothershipChatRead } from '@/hooks/queries/mothership-chats'
70-
import type { PrepareChatSubmission } from '@/hooks/use-composer-submission'
70+
import { type PrepareChatSubmission, useComposerSubmission } from '@/hooks/use-composer-submission'
7171
import { useHydrated } from '@/hooks/use-hydrated'
7272
import { useMothershipDraftsStore } from '@/stores/mothership-drafts/store'
7373
import type { ChatContext } from '@/stores/panel'
@@ -289,8 +289,10 @@ function OrganizationConversationContent({
289289
}
290290
}
291291
const fileDropTarget = useRef<HTMLElement>(null)
292+
const submission = useComposerSubmission({ mode: requestMode })
292293
const files = useFileAttachments({
293294
fileDropTarget: compact ? fileDropTarget : undefined,
295+
disabled: submission.pending,
294296
userId: session?.user?.id,
295297
organizationId: organization.id,
296298
requestMode,
@@ -458,6 +460,7 @@ function OrganizationConversationContent({
458460
const composer = (
459461
<Composer
460462
requestMode={requestMode}
463+
submission={submission}
461464
compact={Boolean(panelOptions)}
462465
attachments={panelOptions?.attachments}
463466
resourceActions={panelOptions?.resourceActions}

‎apps/sim/app/o/[organizationId]/home/components/composer/composer.test.tsx‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
1515
import { createRoot, type Root } from 'react-dom/client'
1616
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
17+
import { useComposerSubmission } from '@/hooks/use-composer-submission'
1718
import type { useSpeechToText } from '@/hooks/use-speech-to-text'
1819
import {
1920
useMothershipEffortStore,
@@ -202,14 +203,17 @@ async function render(
202203
) {
203204
function Harness() {
204205
const [value, setValue] = useState(initialValue)
206+
const submission = useComposerSubmission({ mode: requestMode })
205207
const files = useFileAttachments({
206208
userId: 'user-a',
207209
organizationId: 'organization-a',
208210
requestMode,
211+
disabled: submission.pending,
209212
})
210213
return (
211214
<Composer
212215
requestMode={requestMode}
216+
submission={submission}
213217
restoredContexts={controls.restoredContexts}
214218
value={value}
215219
files={files}
@@ -292,14 +296,17 @@ it.each([false, true])(
292296
: []),
293297
])
294298
}
299+
const submission = useComposerSubmission({ mode: 'agent' })
295300
const files = useFileAttachments({
296301
userId: 'user-a',
297302
organizationId: 'organization-a',
298303
requestMode: 'agent',
304+
disabled: submission.pending,
299305
})
300306
return (
301307
<Composer
302308
requestMode='agent'
309+
submission={submission}
303310
value={value}
304311
onChange={setValue}
305312
restoredContexts={contexts}

‎apps/sim/app/o/[organizationId]/home/components/composer/composer.tsx‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ import { getSkillsQueryOptions } from '@/hooks/queries/skills'
3838
import { useWorkspacesQuery } from '@/hooks/queries/workspace'
3939
import { useAnimatedPlaceholder } from '@/hooks/use-animated-placeholder'
4040
import { useChatInputFocus } from '@/hooks/use-chat-input-focus'
41-
import {
42-
type PendingComposerSubmission,
41+
import type {
42+
PendingComposerSubmission,
4343
useComposerSubmission,
4444
} from '@/hooks/use-composer-submission'
4545
import { useVoiceInput } from '@/hooks/use-voice-input'
@@ -50,6 +50,7 @@ interface ComposerProps {
5050
onPlanChange?: () => void
5151
value: string
5252
files: ReturnType<typeof useFileAttachments>
53+
submission: ReturnType<typeof useComposerSubmission>
5354
/** On the empty home the placeholder types itself and the field is taller; in a chat it is the plain footer input. */
5455
isInitialView: boolean
5556
isSending: boolean
@@ -76,6 +77,7 @@ export function Composer({
7677
onPlanChange,
7778
value,
7879
files,
80+
submission,
7981
isInitialView,
8082
isSending,
8183
onChange,
@@ -174,9 +176,7 @@ export function Composer({
174176
)
175177
const placeholder = isInitialView && !compact ? animatedPlaceholder : 'Send message to Sim'
176178

177-
const { pending: preparingSubmission, accept: acceptSubmission } = useComposerSubmission({
178-
mode: requestMode,
179-
})
179+
const { pending: preparingSubmission, accept: acceptSubmission } = submission
180180
const submit = () => {
181181
if (attachedFilesRef.current.some((file) => file.uploading)) return
182182
const hasPayload =

‎apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -133,10 +133,15 @@ const UserInputImpl = forwardRef<UserInputHandle, UserInputProps>(function UserI
133133

134134
const prevDefaultValueRef = useRef(defaultValue)
135135

136+
const { pending: preparingSubmission, accept: acceptSubmission } = useComposerSubmission({
137+
scope: draftScopeKey,
138+
mode: requestMode,
139+
})
136140
const files = useFileAttachments({
137141
fileDropTarget,
138142
userId,
139143
workspaceId,
144+
disabled: preparingSubmission,
140145
})
141146
const hasFiles = files.attachedFiles.some((f) => !f.uploading && f.key)
142147
const hasUploadingFiles = files.attachedFiles.some((f) => f.uploading)
@@ -535,10 +540,6 @@ const UserInputImpl = forwardRef<UserInputHandle, UserInputProps>(function UserI
535540
filesRef.current = { ...filesRef.current, attachedFiles: [] }
536541
}, [resetTranscript, history.reset])
537542

538-
const { pending: preparingSubmission, accept: acceptSubmission } = useComposerSubmission({
539-
scope: draftScopeKey,
540-
mode: requestMode,
541-
})
542543
const handleSubmit = useCallback(() => {
543544
if (chatPlanRequired) {
544545
useChatPlanModalStore.getState().open(workspaceId)

‎apps/sim/hooks/use-chat-input-focus.ts‎

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
import { createContext, type RefObject, useContext, useEffect, useImperativeHandle } from 'react'
22
import { isMobileViewport } from '@/hooks/use-is-mobile'
33

4-
export const ChatInputFocusContext = createContext<RefObject<(() => void) | null> | null>(null)
4+
export interface ChatInputFocusHandle {
5+
focus: () => void
6+
isFocused: () => boolean
7+
}
8+
9+
export const ChatInputFocusContext = createContext<RefObject<ChatInputFocusHandle | null> | null>(
10+
null
11+
)
512

613
interface UseChatInputFocusProps {
714
textareaRef: RefObject<HTMLTextAreaElement | null>
@@ -12,9 +19,13 @@ export function useChatInputFocus({ textareaRef }: UseChatInputFocusProps) {
1219
const requestedFocus = useContext(ChatInputFocusContext)
1320
useImperativeHandle(
1421
requestedFocus,
15-
() => () => {
16-
if (!document.querySelector('[role="dialog"], dialog[open]')) textareaRef.current?.focus()
17-
},
22+
() => ({
23+
focus: () => {
24+
if (!document.querySelector('[role="dialog"], dialog[open]')) textareaRef.current?.focus()
25+
},
26+
isFocused: () =>
27+
textareaRef.current !== null && document.activeElement === textareaRef.current,
28+
}),
1829
[textareaRef]
1930
)
2031
useEffect(() => {

0 commit comments

Comments
 (0)