Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions apps/extension/src/capture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { extractPageText } from '@/extract'
import { digestUrl } from '@/utils'

/** A public capture error whose message is safe to share with the hosted composer. */
export const PAGE_CHANGED_MESSAGE = 'The page changed. Attach it again.'

export class PageCaptureError extends Error {}

/** Chrome scripting failures may contain the full private URL of the tab. */
Expand Down Expand Up @@ -49,7 +51,7 @@ export async function captureActivePage(
if (tab?.id === undefined || tab.incognito)
throw new PageCaptureError('This page cannot be attached.')
if (expected && expected.tabId !== tab.id) {
throw new PageCaptureError('The page changed. Attach it again.')
throw new PageCaptureError(PAGE_CHANGED_MESSAGE)
}
if (!tab.url) {
const allowed = await chrome.permissions.contains({ origins: PAGE_ACCESS_ORIGINS })
Expand All @@ -75,10 +77,10 @@ export async function captureActivePage(
signal?.addEventListener('abort', removeListeners, { once: true })
try {
const snapshot = await readTab({ tabId: tab.id, url: tab.url }, kind, expected, signal)
if (changed) throw new PageCaptureError('The page changed. Attach it again.')
if (changed) throw new PageCaptureError(PAGE_CHANGED_MESSAGE)
return snapshot
} catch (error) {
if (changed) throw new PageCaptureError('The page changed. Attach it again.')
if (changed) throw new PageCaptureError(PAGE_CHANGED_MESSAGE)
if (expected && !(error instanceof PageCaptureError))
throw new PageCaptureError('The original page is unavailable. Attach the current page.')
throw error
Expand All @@ -95,7 +97,7 @@ async function readTab(
) {
const url = new URL(tab.url)
if (expected && (await digestUrl(tab.url)) !== expected.urlDigest) {
throw new PageCaptureError('The page changed. Attach it again.')
throw new PageCaptureError(PAGE_CHANGED_MESSAGE)
}
let pathname = url.pathname
if (url.origin === SIM_ORIGIN) {
Expand Down Expand Up @@ -132,12 +134,12 @@ async function readTab(
world: 'ISOLATED',
})
if (identity[0]?.documentId !== result.documentId || identity[0]?.result !== tab.url) {
throw new PageCaptureError('The page changed. Attach it again.')
throw new PageCaptureError(PAGE_CHANGED_MESSAGE)
}
const [current] = await chrome.tabs.query({ active: true, currentWindow: true })
signal?.throwIfAborted()
if (current?.id !== tab.tabId || current.url !== tab.url || current.status === 'loading') {
throw new PageCaptureError('The page changed. Attach it again.')
throw new PageCaptureError(PAGE_CHANGED_MESSAGE)
}
if (result.result.error) {
if (kind === 'selection')
Expand Down
4 changes: 3 additions & 1 deletion apps/extension/src/reference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {
} from '@/capture'
import { REFERENCE_PAGE_KEY } from '@/config'

export const PAGE_ATTACHMENT_PENDING_MESSAGE = 'Wait for the page attachment, then try again.'

interface ReferenceOptions {
session: () => string | null
post: (message: ExtensionToAppMessage) => void
Expand Down Expand Up @@ -124,7 +126,7 @@ export function createReferenceCapture({
replyError(
sessionId,
requestId,
manualError?.message ?? 'Wait for the page attachment, then try again.',
manualError?.message ?? PAGE_ATTACHMENT_PENDING_MESSAGE,
manualError?.reason
)
return
Expand Down
20 changes: 10 additions & 10 deletions apps/extension/src/sidepanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,19 @@ import { createRoot } from 'react-dom/client'
import {
captureActivePage,
captureErrorMessage,
PAGE_CHANGED_MESSAGE,
PageCaptureError,
SiteAccessRequiredError,
UnreadableSelectionError,
} from '@/capture'
import { CAPTURE_INTENT_KEY, CAPTURE_INTENT_TTL_MS, REFERENCE_PAGE_KEY, SIM_ORIGIN } from '@/config'
import { type CaptureIntent, captureIntentSchema } from '@/intent'
import { createReferenceCapture } from '@/reference'
import { createReferenceCapture, PAGE_ATTACHMENT_PENDING_MESSAGE } from '@/reference'
import { SearchRecoveryDialog } from '@/search-recovery-dialog'
import { PageAccessButton, SiteAccessDialog } from '@/site-access'
import '@/shell.css'

const PAGE_ATTACHMENT_EXPIRED_MESSAGE = 'The page attachment expired. Attach it again.'
const BLOCKED_SELECTION =
'Select readable text again, attach the page explicitly, or turn off Reference current page.'
const KEYBOARD_FOCUS_HINT = navigator.platform.startsWith('Mac')
Expand Down Expand Up @@ -49,9 +51,7 @@ function SidePanel({ origin }: SidePanelProps) {
if (!(status instanceof SiteAccessRequiredError)) return
const navigated = (tabId: number, change: chrome.tabs.OnUpdatedInfo) => {
if (tabId === status.tabId && (change.status === 'loading' || change.url !== undefined)) {
setStatus((current) =>
current === status ? 'The page changed. Attach it again.' : current
)
setStatus((current) => (current === status ? PAGE_CHANGED_MESSAGE : current))
}
}
const activated = (tab: chrome.tabs.OnActivatedInfo) => {
Expand All @@ -74,7 +74,7 @@ function SidePanel({ origin }: SidePanelProps) {
if (Date.now() - Date.parse(snapshot.capturedAt) > CAPTURE_INTENT_TTL_MS) {
pendingRef.current = null
focusRequested.current = false
setStatus('The page attachment expired. Attach it again.')
setStatus(PAGE_ATTACHMENT_EXPIRED_MESSAGE)
return
}
const message: ExtensionToAppMessage = {
Expand Down Expand Up @@ -130,12 +130,12 @@ function SidePanel({ origin }: SidePanelProps) {
}
) {
if (referenceRef.current?.busy()) {
setStatus('Wait for the page attachment, then try again.')
setStatus(PAGE_ATTACHMENT_PENDING_MESSAGE)
return
}
if (busyRef.current) return
if (pendingRef.current) {
setStatus('Wait for the page attachment, then try again.')
setStatus(PAGE_ATTACHMENT_PENDING_MESSAGE)
return
}
busyRef.current = true
Expand Down Expand Up @@ -171,7 +171,7 @@ function SidePanel({ origin }: SidePanelProps) {
manualCaptureError: () => {
if (busyRef.current || pendingRef.current || processingManual || manualIntents.size)
return {
message: 'Wait for the page attachment, then try again.',
message: PAGE_ATTACHMENT_PENDING_MESSAGE,
}
if (unreadableSelection.current) return { message: BLOCKED_SELECTION, reason: 'selection' }
},
Expand Down Expand Up @@ -249,7 +249,7 @@ function SidePanel({ origin }: SidePanelProps) {
}
if (disposed || !intent) return
if (!currentIntent(intent, window.id)) {
setStatus('The page attachment expired. Attach it again.')
setStatus(PAGE_ATTACHMENT_EXPIRED_MESSAGE)
return
}
if (intent.kind === 'focus') {
Expand Down Expand Up @@ -362,7 +362,7 @@ function SidePanel({ origin }: SidePanelProps) {
) {
pendingRef.current = null
focusRequested.current = false
setStatus('The page attachment expired. Attach it again.')
setStatus(PAGE_ATTACHMENT_EXPIRED_MESSAGE)
}
void consumeIntent()
}, 5_000)
Expand Down
20 changes: 8 additions & 12 deletions apps/extension/src/site-access.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'react'
import { useEffect, useState } from 'react'
import { Chip, ChipModalBody, ChipModalHeader, ChipModalSurface } from '@sim/emcn'
import { PAGE_ACCESS_ORIGINS } from '@/config'

Expand All @@ -8,25 +8,17 @@ interface PageAccessButtonProps {
}

export function PageAccessButton({ allowed, onAccessGranted }: PageAccessButtonProps) {
const mounted = useRef(false)
const [status, setStatus] = useState<'idle' | 'requesting' | 'error'>('idle')

useEffect(() => {
mounted.current = true
return () => {
mounted.current = false
}
}, [])

async function enable() {
if (status === 'requesting') return
setStatus('requesting')
try {
const granted = await chrome.permissions.request({ origins: PAGE_ACCESS_ORIGINS })
if (mounted.current) setStatus('idle')
setStatus('idle')
if (granted) onAccessGranted()
} catch {
if (mounted.current) setStatus('error')
setStatus('error')
}
}

Expand All @@ -47,7 +39,11 @@ export function PageAccessButton({ allowed, onAccessGranted }: PageAccessButtonP
<Chip disabled={status === 'requesting'} onClick={() => void enable()}>
Allow across websites
</Chip>
{status === 'error' && <span role='status'>Could not enable page access. Try again.</span>}
{status === 'error' && (
<span role='status' className='text-[var(--text-error)] text-caption'>
Could not enable page access. Try again.
</span>
)}
</span>
)
}
Expand Down
7 changes: 2 additions & 5 deletions apps/sim/app/extension/[workspaceId]/workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import { Chip, ChipLink } from '@sim/emcn'
import { isApiClientError } from '@/lib/api/client/errors'
import { ExtensionChat } from '@/app/extension/[workspaceId]/chat'
import ExtensionLoading from '@/app/extension/loading'
import { useExtensionIdentity } from '@/app/extension/providers'
import { ExtensionSignIn } from '@/app/extension/sign-in'
import { ExtensionSessionExpired } from '@/app/extension/providers'
import { ChatPlanModal } from '@/app/workspace/[workspaceId]/home/components/chat-plan-modal'
import { BlockVisibilityLoader } from '@/app/workspace/[workspaceId]/providers/block-visibility-loader'
import { CustomBlocksLoader } from '@/app/workspace/[workspaceId]/providers/custom-blocks-loader'
Expand All @@ -26,13 +25,11 @@ interface ExtensionWorkspaceProps {
}

export function ExtensionWorkspace({ workspaceId }: ExtensionWorkspaceProps) {
const { userId } = useExtensionIdentity()
const { data, isPending, error, refetch } = useWorkspaceHostContextQuery(workspaceId, {
refetchInterval: WORKSPACE_HOST_CONTEXT_STALE_TIME,
refetchOnWindowFocus: true,
})
if (isApiClientError(error) && error.status === 401)
return <ExtensionSignIn expectedUserId={userId} />
if (isApiClientError(error) && error.status === 401) return <ExtensionSessionExpired />
if (isPending) return <ExtensionLoading />
const accessDenied = isApiClientError(error) && (error.status === 403 || error.status === 404)
if (accessDenied || !data) {
Expand Down
7 changes: 2 additions & 5 deletions apps/sim/app/extension/o/[organizationId]/organization.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ import { ExtensionHeader } from '@/app/extension/[workspaceId]/header'
import { extensionChatParam } from '@/app/extension/[workspaceId]/search-params'
import { useExtensionComposer } from '@/app/extension/hooks/use-composer'
import ExtensionLoading from '@/app/extension/loading'
import { useExtensionIdentity } from '@/app/extension/providers'
import { ExtensionSignIn } from '@/app/extension/sign-in'
import { ExtensionSessionExpired } from '@/app/extension/providers'
import { ExtensionUnavailable } from '@/app/extension/unavailable'
import { OrganizationConversation } from '@/app/o/[organizationId]/components/organization-conversation'
import { OrganizationProvider } from '@/app/o/[organizationId]/providers/organization-provider'
Expand All @@ -27,14 +26,12 @@ interface ExtensionOrganizationProps {
}

export function ExtensionOrganization({ context }: ExtensionOrganizationProps) {
const { userId } = useExtensionIdentity()
const access = useOrganizationSummary(context.organization.id)
const history = useOrganizationMothershipChats(context.organization.id)
const denied = [access.error, history.error].find(
(error) => isApiClientError(error) && [401, 403, 404].includes(error.status)
)
if (isApiClientError(denied) && denied.status === 401)
return <ExtensionSignIn expectedUserId={userId} />
if (isApiClientError(denied) && denied.status === 401) return <ExtensionSessionExpired />
if (access.isPending) return <ExtensionLoading />
if (denied || !access.data?.chatAccess)
return (
Expand Down
7 changes: 2 additions & 5 deletions apps/sim/app/extension/workspaces.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,14 @@ import {
} from '@sim/emcn'
import { isApiClientError } from '@/lib/api/client/errors'
import ExtensionLoading from '@/app/extension/loading'
import { useExtensionIdentity } from '@/app/extension/providers'
import { ExtensionSignIn } from '@/app/extension/sign-in'
import { ExtensionSessionExpired } from '@/app/extension/providers'
import { useWorkspacesQuery } from '@/hooks/queries/workspace'

export function ExtensionWorkspaces() {
const { userId } = useExtensionIdentity()
const [scrollElement, setScrollElement] = useState<HTMLElement | null>(null)
const scrollEdges = useScrollEdges(scrollElement)
const { data: workspaces, isPending, error, refetch } = useWorkspacesQuery()
if (isApiClientError(error) && error.status === 401)
return <ExtensionSignIn expectedUserId={userId} />
if (isApiClientError(error) && error.status === 401) return <ExtensionSessionExpired />
if (isPending) return <ExtensionLoading />
const transientError =
!isApiClientError(error) || error.status === 408 || error.status === 429 || error.status >= 500
Expand Down
10 changes: 5 additions & 5 deletions apps/sim/hooks/use-speech-to-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ export function useSpeechToText({
: { workspaceId: workspaceIdRef.current },
})
} catch (err) {
if (!mountedRef.current || generation !== generationRef.current) return false
if (generation !== generationRef.current) return false
if (isApiClientError(err) && err.status === 402) {
const isMemberLimit = (err.body as { scope?: string } | null)?.scope === 'member'
onUsageLimitExceededRef.current?.(err.message, isMemberLimit)
Expand All @@ -244,7 +244,7 @@ export function useSpeechToText({

const token = typeof tokenData.token === 'string' ? tokenData.token : undefined
if (!token) throw new Error('Failed to get speech token')
if (!mountedRef.current || generation !== generationRef.current) return false
if (generation !== generationRef.current) return false

const stream = await navigator.mediaDevices.getUserMedia({
audio: {
Expand All @@ -256,7 +256,7 @@ export function useSpeechToText({
},
})

if (!mountedRef.current || generation !== generationRef.current) {
if (generation !== generationRef.current) {
stream.getTracks().forEach((track) => track.stop())
return false
}
Expand All @@ -281,7 +281,7 @@ export function useSpeechToText({
ws.onerror = () => reject(new Error('WebSocket connection failed'))

ws.onmessage = (event) => {
if (!mountedRef.current || generation !== generationRef.current) return
if (generation !== generationRef.current) return
try {
const msg = JSON.parse(event.data)

Expand Down Expand Up @@ -325,7 +325,7 @@ export function useSpeechToText({
}
})

if (!mountedRef.current || generation !== generationRef.current) {
if (generation !== generationRef.current) {
ws.close()
stream.getTracks().forEach((track) => track.stop())
return false
Expand Down
Loading
Loading