Skip to content

Commit 5ce329a

Browse files
committed
fix(extension): attach Sim pages and streamline composer controls
1 parent 5216fd4 commit 5ce329a

10 files changed

Lines changed: 149 additions & 14 deletions

File tree

‎apps/extension/scripts/test-e2e.ts‎

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,12 +129,17 @@ try {
129129
await signIn.close()
130130
const panel: Page = await context.newPage()
131131
// Observe dispatch before the bridge so background metadata queries stay outside each action.
132-
await panel.addInitScript(() => {
132+
const observeDispatch = () => {
133133
window.addEventListener('message', () => {
134134
document.documentElement.dataset.queryCountAtDispatch =
135135
document.documentElement.dataset.browserQueryCount ?? '0'
136136
})
137-
})
137+
}
138+
const panelBundle = resolve(extensionPath, 'sidepanel.js')
139+
await writeFile(
140+
panelBundle,
141+
`(${observeDispatch.toString()})();\n${await readFile(panelBundle, 'utf8')}`
142+
)
138143
await panel.goto(`chrome-extension://${extensionId}/sidepanel.html`)
139144
await panel.getByTitle('Sim chat').waitFor()
140145
const frame: Frame = panel.frames()[1] ?? (await panel.waitForEvent('frameattached'))
@@ -668,6 +673,35 @@ try {
668673
await parentCdp.detach()
669674
}
670675
)
676+
await check('Ordinary Sim pages attach without capturing sensitive fields', async () => {
677+
for (const path of ['/workspace/example', '/extension-notes']) {
678+
const before = (await snapshots()).length
679+
await page.goto(`${origin}${path}?token=URL_SECRET#FRAGMENT_SECRET`)
680+
await page.bringToFront()
681+
await capture()
682+
await expect.poll(async () => (await snapshots()).length).toBe(before + 1)
683+
const snapshot = (await snapshots()).at(-1)
684+
assert(snapshot?.url === `${origin}${path}`, 'Sim page URL retained private parameters')
685+
assert(snapshot.text.includes('visible-reference-482'), 'Visible Sim page content is missing')
686+
assert(!snapshot.text.includes('SENTINEL'), 'Sensitive Sim page content was captured')
687+
}
688+
})
689+
await check('Embedded Sim chat pages cannot capture themselves', async () => {
690+
for (const path of ['/extension', '/extension/workspace', '/extension/o/organization']) {
691+
await page.goto(`${origin}/workspace/example`)
692+
await page.bringToFront()
693+
const previous = (await snapshots()).length
694+
await capture()
695+
await expect.poll(async () => (await snapshots()).length).toBe(previous + 1)
696+
await expect(panel.getByRole('status')).toHaveText('')
697+
const before = (await snapshots()).length
698+
await page.goto(`${origin}${path}`)
699+
await page.bringToFront()
700+
await capture()
701+
await panel.getByRole('status').filter({ hasText: 'Choose a regular web page' }).waitFor()
702+
assert((await snapshots()).length === before, 'Embedded chat was captured')
703+
}
704+
})
671705
await check(
672706
'Site access stays modal without violating the packaged security policy',
673707
async () => {

‎apps/extension/src/capture.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,13 @@ export async function captureActivePage(
2323
}
2424
if (
2525
!['http:', 'https:'].includes(url.protocol) ||
26-
url.origin === SIM_ORIGIN ||
26+
(url.origin === SIM_ORIGIN &&
27+
(url.pathname === '/extension' || url.pathname.startsWith('/extension/'))) ||
2728
url.username ||
2829
url.password ||
2930
tab.incognito
3031
) {
31-
throw new Error('Choose a regular web page outside Sim to attach.')
32+
throw new Error('Choose a regular web page to attach.')
3233
}
3334
const results = await chrome.scripting.executeScript({
3435
target: expected

‎apps/sim/app/extension/[workspaceId]/chat.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ export function ExtensionChat({ workspaceId, workspaceName }: ExtensionChatProps
169169
draftScopeKey={`extension:${userId}:${workspaceId}:${selectedChatId ?? 'new'}`}
170170
composerAttachments={composer.attachments}
171171
composerResourceActions={composer.resourceActions}
172+
composerToolbarActions={composer.toolbarActions}
172173
composerModelSelectorOptions={composer.modelSelectorOptions}
173174
layout='copilot-view'
174175
className={cn(

‎apps/sim/app/extension/hooks/use-composer.tsx‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client'
22

33
import { useCallback, useMemo } from 'react'
4-
import { DropdownMenuItem, toast } from '@sim/emcn'
4+
import { Chip, DropdownMenuItem, Tooltip, toast } from '@sim/emcn'
55
import { Globe, Highlighter, Settings } from '@sim/emcn/icons'
66
import { getPageContextBudgetError } from '@/lib/mothership/chat/page-context'
77
import { usePageContext } from '@/app/extension/hooks/use-page-context'
@@ -77,9 +77,27 @@ export function useExtensionComposer(scopeKey: string) {
7777
},
7878
[snapshots]
7979
)
80+
const toolbarActions = useMemo(
81+
() =>
82+
connected ? (
83+
<Tooltip.Root>
84+
<Tooltip.Trigger asChild>
85+
<Chip
86+
shape='round'
87+
leftIcon={Globe}
88+
onClick={() => requestAction('capture-page')}
89+
aria-label='Attach current page'
90+
/>
91+
</Tooltip.Trigger>
92+
<Tooltip.Content side='top'>Attach current page</Tooltip.Content>
93+
</Tooltip.Root>
94+
) : undefined,
95+
[connected, requestAction]
96+
)
8097
return {
8198
attachments,
8299
resourceActions,
100+
toolbarActions,
83101
modelSelectorOptions: MODEL_SELECTOR_OPTIONS,
84102
prepareSubmission,
85103
onSubmitted: clearSnapshots,

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ interface OrganizationConversationProps {
9494
header: ReactNode
9595
attachments?: ReactNode
9696
resourceActions?: ReactNode
97+
toolbarActions?: ReactNode
9798
modelSelectorOptions?: ModelSelectorOptions
9899
onChatIdResolved: (chatId: string) => void
99100
onNavigateToChat: (chatId: string) => void
@@ -453,6 +454,7 @@ function OrganizationConversationContent({
453454
compact={Boolean(panelOptions)}
454455
attachments={panelOptions?.attachments}
455456
resourceActions={panelOptions?.resourceActions}
457+
toolbarActions={panelOptions?.toolbarActions}
456458
modelSelectorOptions={panelOptions?.modelSelectorOptions}
457459
onPlanChange={
458460
!hasChat && destinationMode === 'agent' && planEnabled

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ interface ComposerProps {
5959
compact?: boolean
6060
attachments?: ReactNode
6161
resourceActions?: ReactNode
62+
toolbarActions?: ReactNode
6263
modelSelectorOptions?: ModelSelectorOptions
6364
}
6465

@@ -80,6 +81,7 @@ export function Composer({
8081
compact = false,
8182
attachments,
8283
resourceActions,
84+
toolbarActions,
8385
modelSelectorOptions,
8486
}: ComposerProps) {
8587
const attachedFilesRef = useRef(files.attachedFiles)
@@ -257,6 +259,7 @@ export function Composer({
257259
</Tooltip.Root>
258260
)}
259261
{!imagesOnly && contextPicker('skills', Slash, 'Skills')}
262+
{toolbarActions}
260263
{onPlanChange && (
261264
<Chip
262265
shape='round'

‎apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ interface MothershipChatProps {
8888
composer?: ReactNode
8989
composerAttachments?: ReactNode
9090
composerResourceActions?: ReactNode
91+
composerToolbarActions?: ReactNode
9192
composerModelSelectorOptions?: ModelSelectorOptions
9293
messages: ChatMessage[]
9394
isSending: boolean
@@ -393,6 +394,7 @@ export function MothershipChat({
393394
composer,
394395
composerAttachments,
395396
composerResourceActions,
397+
composerToolbarActions,
396398
composerModelSelectorOptions,
397399
messages: messagesProp,
398400
isSending,
@@ -1052,6 +1054,7 @@ export function MothershipChat({
10521054
onSubmit={onSubmit}
10531055
attachments={composerAttachments}
10541056
resourceActions={composerResourceActions}
1057+
toolbarActions={composerToolbarActions}
10551058
modelSelectorOptions={composerModelSelectorOptions}
10561059
isSending={isStreamActive}
10571060
onStopGeneration={onStopGeneration}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ export const AttachedFilesList = React.memo(function AttachedFilesList({
170170
<div
171171
ref={scrollRef}
172172
className={cn(
173-
'mb-1.5 max-md:max-h-[102px] max-md:overflow-y-auto max-md:overscroll-contain',
173+
'mb-1.5 max-md:pointer-coarse:max-h-[102px] max-md:pointer-coarse:overflow-y-auto max-md:pointer-coarse:overscroll-contain',
174174
scrollFadeClass
175175
)}
176176
{...scrollFadeAttributes(edges)}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ const logger = createLogger('UserInput')
6060
interface UserInputProps {
6161
attachments?: React.ReactNode
6262
resourceActions?: React.ReactNode
63+
toolbarActions?: React.ReactNode
6364
modelSelectorOptions?: ModelSelectorOptions
6465
requestMode?: 'agent' | 'plan'
6566
onPlanChange?: () => void
@@ -98,6 +99,7 @@ const UserInputImpl = forwardRef<UserInputHandle, UserInputProps>(function UserI
9899
{
99100
attachments,
100101
resourceActions,
102+
toolbarActions,
101103
modelSelectorOptions,
102104
requestMode = 'agent',
103105
onPlanChange,
@@ -679,6 +681,7 @@ const UserInputImpl = forwardRef<UserInputHandle, UserInputProps>(function UserI
679681
</Tooltip.Trigger>
680682
<Tooltip.Content side='top'>Skills</Tooltip.Content>
681683
</Tooltip.Root>
684+
{toolbarActions}
682685
{onPlanChange && (
683686
<Chip
684687
shape='round'

‎apps/sim/scripts/test-browser-extension-e2e.ts‎

Lines changed: 78 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -446,16 +446,20 @@ try {
446446
})
447447
}, base.origin)
448448
await observeCaptureReceipts()
449-
const capture = async (mode: 'page' | 'selection' = 'page') => {
449+
const capture = async (mode: 'page' | 'selection' | 'shortcut' = 'page') => {
450450
await source.bringToFront()
451451
const before = Number(await shell.locator('body').getAttribute('data-capture-receipts'))
452-
await frame().getByRole('button', { name: 'Add resources', exact: true }).press('Enter')
453-
await frame()
454-
.getByRole('menuitem', {
455-
name: mode === 'page' ? 'Current page' : 'Selected text',
456-
exact: true,
457-
})
458-
.press('Enter')
452+
if (mode === 'shortcut') {
453+
await frame().getByRole('button', { name: 'Attach current page', exact: true }).press('Enter')
454+
} else {
455+
await frame().getByRole('button', { name: 'Add resources', exact: true }).press('Enter')
456+
await frame()
457+
.getByRole('menuitem', {
458+
name: mode === 'page' ? 'Current page' : 'Selected text',
459+
exact: true,
460+
})
461+
.press('Enter')
462+
}
459463
await expect(shell.locator('body')).toHaveAttribute('data-capture-receipts', String(before + 1))
460464
}
461465
const attachments = () => frame().getByRole('group', { name: 'Page attached to next message' })
@@ -772,7 +776,51 @@ try {
772776
async () => {
773777
await capture()
774778
await attachFile()
779+
await shell.setViewportSize({ width: 320, height: 844 })
780+
await capture()
781+
await capture()
782+
const geometry = await attachments()
783+
.first()
784+
.evaluate((chip) => {
785+
const strip = chip.parentElement?.parentElement
786+
if (!strip) throw new Error('Missing attachment strip')
787+
return { height: strip.clientHeight, contentHeight: strip.scrollHeight }
788+
})
789+
assert(geometry.contentHeight > 102, 'The fixture did not exercise multiple attachment rows')
790+
assert(
791+
geometry.contentHeight <= geometry.height + 1,
792+
'Desktop attachments require nested scrolling'
793+
)
775794
await screenshot('mixed-attachments')
795+
const device = await browserContext.newCDPSession(frame())
796+
try {
797+
await device.send('Emulation.setTouchEmulationEnabled', {
798+
enabled: true,
799+
maxTouchPoints: 1,
800+
})
801+
await expect
802+
.poll(() => frame().evaluate(() => matchMedia('(pointer: coarse)').matches))
803+
.toBe(true)
804+
const touchGeometry = await attachments()
805+
.first()
806+
.evaluate((chip) => {
807+
const strip = chip.parentElement?.parentElement
808+
if (!strip) throw new Error('Missing attachment strip')
809+
return { height: strip.clientHeight, contentHeight: strip.scrollHeight }
810+
})
811+
assert(touchGeometry.height <= 102, 'Touch attachments lost their height limit')
812+
assert(
813+
touchGeometry.contentHeight > touchGeometry.height,
814+
'Touch overflow fixture did not wrap'
815+
)
816+
await screenshot('touch-attachments')
817+
} finally {
818+
await device.send('Emulation.setTouchEmulationEnabled', { enabled: false })
819+
await device.detach()
820+
}
821+
await attachments().last().getByRole('button', { name: 'Remove page context' }).click()
822+
await attachments().last().getByRole('button', { name: 'Remove page context' }).click()
823+
await shell.setViewportSize({ width: 390, height: 844 })
776824
await attachments().first().getByRole('button', { name: 'Remove page context' }).click()
777825
await expect(
778826
frame().getByRole('button', { name: 'Remove extension-notes.txt', exact: true })
@@ -1223,6 +1271,25 @@ try {
12231271
await screenshot('organization-first-open-capture')
12241272
}
12251273
)
1274+
await checkNative('The current-page shortcut attaches in both shared composers', async () => {
1275+
for (const path of [`/extension/${alphaId}`, `/extension/o/${organizationId}`]) {
1276+
await navigate(`${base.origin}${path}`)
1277+
await expect(attachments()).toHaveCount(0)
1278+
await capture('shortcut')
1279+
await expect(attachments()).toHaveCount(1)
1280+
await attachments()
1281+
.first()
1282+
.getByRole('button', { name: /^Review page context:/ })
1283+
.click()
1284+
const preview = frame().getByRole('dialog', { name: 'Page context', exact: true })
1285+
await expect(preview.locator('pre')).toContainText(captureText)
1286+
await preview.getByRole('button', { name: 'Close', exact: true }).click()
1287+
await frame().getByRole('button', { name: 'Attach current page', exact: true }).hover()
1288+
await expect(frame().getByRole('tooltip', { name: 'Attach current page' })).toBeVisible()
1289+
await screenshot(path.includes('/o/') ? 'shortcut-organization-agent' : 'shortcut-workspace')
1290+
await attachments().first().getByRole('button', { name: 'Remove page context' }).click()
1291+
}
1292+
})
12261293
if (liveChat) {
12271294
await checkNative(
12281295
'Organization live admission preserves queued pages, files, and the admitted conversation mode',
@@ -1379,6 +1446,9 @@ try {
13791446
await screenshot('organization-mode-file-recovery')
13801447
await frame().getByRole('button', { name: 'Remove extension-notes.txt', exact: true }).click()
13811448
await expect(frame().getByText(/Remove unsupported files/)).not.toBeVisible()
1449+
await attachments().first().getByRole('button', { name: 'Remove page context' }).click()
1450+
await capture('shortcut')
1451+
await expect(attachments()).toHaveCount(1)
13821452
await screenshot('organization-assistant-page')
13831453
if (!liveChat)
13841454
await shell.route('**/api/mothership/chat', (route) =>

0 commit comments

Comments
 (0)