Skip to content

Commit 145901b

Browse files
committed
fix(extension): align shared chat and harden capture lifecycle
1 parent 6b4a92a commit 145901b

41 files changed

Lines changed: 1541 additions & 462 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/ci-suites.json‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616
"group": "desktop-inbox",
1717
"task": "ci:e2e-desktop-inbox"
1818
},
19+
{
20+
"group": "browser-extension",
21+
"task": "ci:e2e-browser-extension"
22+
},
1923
{
2024
"group": "seo",
2125
"task": "ci:e2e-seo"

‎.github/scripts/ci-plan.gql‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ query($base: String!, $head: String!) {
33
affectedTasks(base: $base, head: $head, tasks: [
44
"test", "build", "type-check", "ci:integration", "ci:desktop-live",
55
"ci:e2e-scim", "ci:e2e-cli", "ci:e2e-stop-after", "ci:e2e-desktop-inbox",
6-
"ci:e2e-project-files", "ci:e2e-mobile", "ci:e2e-seo"
6+
"ci:e2e-browser-extension", "ci:e2e-project-files", "ci:e2e-mobile", "ci:e2e-seo"
77
]) {
88
items {
99
name

‎.github/scripts/http-e2e.sh‎

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# CI groups, which reuse the production build from `e2e-build`.
44
# The job extracts it into apps/sim/.next before running these suites.
55
#
6-
# Usage: http-e2e.sh <scim|cli|stop-after|desktop-inbox|project-files|mobile|seo> (run from apps/sim)
6+
# Usage: http-e2e.sh <scim|cli|stop-after|desktop-inbox|browser-extension|project-files|mobile|seo> (run from apps/sim)
77
#
88
# The job provides DATABASE_URL, BETTER_AUTH_SECRET and ENCRYPTION_KEY; each group sets the rest of
99
# its app's environment here. Reports and server logs land in $RUNNER_TEMP/e2e.
@@ -20,7 +20,7 @@
2020
# telemetry flush runs detached and still writes .next/dev).
2121
set -euo pipefail
2222

23-
group=${1:?usage: http-e2e.sh <scim|cli|stop-after|desktop-inbox|project-files|mobile|seo>}
23+
group=${1:?usage: http-e2e.sh <scim|cli|stop-after|desktop-inbox|browser-extension|project-files|mobile|seo>}
2424
report_dir="$RUNNER_TEMP/e2e"
2525
ready_timeout_seconds=600
2626
mkdir -p "$report_dir"
@@ -198,7 +198,7 @@ case "$group" in
198198
;;
199199

200200
# The desktop background executor's protocol: device registration, the SSE doorbell over Redis
201-
# pub/sub, presence, leased claims, Stop and isolation. The only group whose app gets Redis.
201+
# pub/sub, presence, leased claims, Stop and isolation.
202202
desktop-inbox)
203203
export REDIS_URL=redis://127.0.0.1:6379
204204
export NEXT_PUBLIC_FORCE_HOSTED=false
@@ -228,6 +228,31 @@ case "$group" in
228228
bun --no-env-file scripts/test-organization-search-e2e.ts
229229
;;
230230

231+
browser-extension)
232+
export REDIS_URL=redis://127.0.0.1:6379
233+
export NEXT_PUBLIC_FORCE_HOSTED=false
234+
export KNOWLEDGE_MEMBER_ACCESS=true CREDENTIAL_GROUPS=true ACCESS_CONTROL_ENABLED=true
235+
export NEXT_PUBLIC_ACCESS_CONTROL_ENABLED=true
236+
export API_ENCRYPTION_KEY="$ENCRYPTION_KEY"
237+
bunx --no-install playwright install --with-deps chromium
238+
extension_identity=$(bun - <<'JS'
239+
import { generateKeyPairSync, createHash } from 'node:crypto'
240+
const { publicKey } = generateKeyPairSync('rsa', { modulusLength: 2048 })
241+
const der = publicKey.export({ type: 'spki', format: 'der' })
242+
const id = [...createHash('sha256').update(der).digest('hex').slice(0, 32)]
243+
.map(c => String.fromCharCode(97 + parseInt(c, 16))).join('')
244+
process.stdout.write(`${der.toString('base64')}\n${id}`)
245+
JS
246+
)
247+
export SIM_EXTENSION_KEY="${extension_identity%%$'\n'*}"
248+
export BROWSER_EXTENSION_IDS="${extension_identity#*$'\n'}"
249+
start_app browser-extension 3007 'browser extension' record-http-status
250+
EXTENSION_APP_E2E_BASE_URL="$local_app_url" \
251+
EXTENSION_APP_E2E_DATABASE_URL="$DATABASE_URL" \
252+
EXTENSION_APP_E2E_REPORT_PATH="$report_dir/browser-extension/report.json" \
253+
bun run test:browser-extension:e2e
254+
;;
255+
231256
project-files)
232257
PROJECT_FILES_E2E_ADMIN_DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:5432/postgres \
233258
PROJECT_FILES_E2E_REDIS_URL=redis://127.0.0.1:6379 \

‎.github/workflows/browser-extension.yml‎

Lines changed: 3 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -28,42 +28,9 @@ concurrency:
2828
cancel-in-progress: true
2929

3030
jobs:
31-
browser:
32-
runs-on: ubuntu-latest
33-
timeout-minutes: 20
34-
services:
35-
postgres:
36-
image: pgvector/pgvector:pg17
37-
env:
38-
POSTGRES_USER: postgres
39-
POSTGRES_PASSWORD: postgres
40-
POSTGRES_DB: extension_test
41-
ports:
42-
- 5432:5432
43-
options: >-
44-
--health-cmd "pg_isready -U postgres -d extension_test"
45-
--health-interval 5s
46-
--health-timeout 5s
47-
--health-retries 10
48-
redis:
49-
image: redis:8.2-alpine
50-
ports:
51-
- 6379:6379
52-
options: >-
53-
--health-cmd "redis-cli ping"
54-
--health-interval 5s
55-
--health-timeout 5s
56-
--health-retries 10
57-
env:
58-
DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/extension_test
59-
REDIS_URL: redis://127.0.0.1:6379
60-
NEXT_PUBLIC_APP_URL: http://127.0.0.1:3007
61-
BETTER_AUTH_URL: http://127.0.0.1:3007
62-
BETTER_AUTH_SECRET: browser-extension-ci-auth-secret-32-characters
63-
ENCRYPTION_KEY: '0000000000000000000000000000000000000000000000000000000000000000'
64-
API_ENCRYPTION_KEY: '1111111111111111111111111111111111111111111111111111111111111111'
65-
NEXT_TELEMETRY_DISABLED: '1'
66-
DISABLE_AUTH: 'false'
31+
transport:
32+
runs-on: ubuntu-24.04
33+
timeout-minutes: 10
6734
steps:
6835
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
6936
with:
@@ -81,43 +48,9 @@ jobs:
8148
working-directory: apps/extension
8249
env:
8350
EXTENSION_REPORT_PATH: test-results/extension-report.json
84-
- name: Provision the disposable app database
85-
run: bun run --cwd packages/db db:migrate
86-
- name: Generate the fixture extension identity
87-
run: |
88-
bun - <<'JS' >> "$GITHUB_ENV"
89-
import { generateKeyPairSync, createHash } from "node:crypto";
90-
const { publicKey } = generateKeyPairSync("rsa", { modulusLength: 2048 });
91-
const der = publicKey.export({ type: "spki", format: "der" });
92-
const id = [...createHash("sha256").update(der).digest("hex").slice(0, 32)]
93-
.map(c => String.fromCharCode(97 + parseInt(c, 16))).join("");
94-
process.stdout.write(`SIM_EXTENSION_KEY=${der.toString("base64")}\nBROWSER_EXTENSION_IDS=${id}\n`);
95-
JS
96-
- name: Start the real Sim app
97-
working-directory: apps/sim
98-
run: |
99-
node "$(bun -p 'require.resolve("next/dist/bin/next")')" dev --hostname 127.0.0.1 --port 3007 > "$RUNNER_TEMP/extension-app.log" 2>&1 &
100-
for ((attempt=0; attempt<60; attempt++)); do
101-
if curl --fail --silent http://127.0.0.1:3007/api/auth/get-session > /dev/null; then exit 0; fi
102-
sleep 2
103-
done
104-
exit 1
105-
- name: Exercise authenticated app integration
106-
working-directory: apps/sim
107-
run: bun run test:browser-extension:e2e
108-
env:
109-
EXTENSION_APP_E2E_BASE_URL: http://127.0.0.1:3007
110-
EXTENSION_APP_E2E_DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/extension_test
111-
EXTENSION_APP_E2E_REPORT_PATH: ../extension/test-results/real-app-report.json
11251
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
11352
if: always()
11453
with:
11554
name: browser-extension-results
11655
path: apps/extension/test-results
11756
retention-days: 7
118-
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
119-
if: failure()
120-
with:
121-
name: browser-extension-app-log
122-
path: ${{ runner.temp }}/extension-app.log
123-
retention-days: 7

‎.github/workflows/checks.yml‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -217,9 +217,9 @@ jobs:
217217
--health-interval 5s
218218
--health-timeout 5s
219219
--health-retries 10
220-
# Desktop inbox and Project Files use Redis.
220+
# Desktop inbox, browser extension, and Project Files use Redis.
221221
redis:
222-
image: ${{ (matrix.group == 'desktop-inbox' || matrix.group == 'project-files') && 'redis:8.2-alpine' || '' }}
222+
image: ${{ (matrix.group == 'desktop-inbox' || matrix.group == 'browser-extension' || matrix.group == 'project-files') && 'redis:8.2-alpine' || '' }}
223223
ports:
224224
- 6379:6379
225225
options: >-
@@ -296,7 +296,9 @@ jobs:
296296
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
297297
with:
298298
name: e2e-reports-${{ matrix.group }}
299-
path: ${{ runner.temp }}/e2e/**/*.json
299+
path: |
300+
${{ runner.temp }}/e2e/**/*.json
301+
${{ runner.temp }}/e2e/browser-extension/**/*.png
300302
if-no-files-found: ignore
301303
retention-days: 7
302304

‎apps/extension/README.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Sim browser extension
22

3-
The Chrome extension attaches an explicitly captured page or selection to the existing Sim chat. The native side panel hosts the authenticated `/extension` app surface. Chat rendering, execution, permissions, billing, and history belong to Sim and Mothership.
3+
The Chrome extension attaches an explicitly captured page or selection to the existing Sim chat. The native side panel hosts the authenticated `/extension` app surface. Chat opens in the user's organization when available; choosing a workspace is optional. Members without access to Home use the organization's Assistant. Chat rendering, execution, permissions, billing, and history belong to Sim and Mothership.
44

55
## Build and load
66

@@ -48,7 +48,7 @@ The E2E suite uses real Chromium, the packaged extension, and a disposable HTTP
4848

4949
Release verification must additionally exercise the native toolbar and shortcut, Chrome's real grant/revoke prompt, sign-in/SSO and expiry against the running Sim app, side-panel closing/reopening, workspace changes, narrow widths, and the real Mothership streaming response. The fixture suite does not establish those integrations. The design document records launch scope and release criteria.
5050

51-
The authenticated acceptance suite runs against a real local app, Better Auth, and a disposable Postgres database whose name contains `test`:
51+
The authenticated acceptance suite runs against a real local app, Better Auth, and a disposable Postgres database whose name contains `test`. Enable `KNOWLEDGE_MEMBER_ACCESS`, `CREDENTIAL_GROUPS`, `ACCESS_CONTROL_ENABLED`, and `NEXT_PUBLIC_ACCESS_CONTROL_ENABLED` on that app to exercise restricted organization members:
5252

5353
```sh
5454
EXTENSION_APP_E2E_BASE_URL=http://127.0.0.1:3007 \
@@ -58,7 +58,7 @@ SIM_EXTENSION_KEY="$PUBLIC_EXTENSION_KEY" \
5858
bun run --cwd apps/sim test:browser-extension:e2e
5959
```
6060

61-
Configure the matching extension ID in the running app. The suite builds into a temporary directory, creates its own accounts/workspaces, and removes them afterward; it leaves the shipping `dist` untouched. It verifies real authentication, workspace denial, page limits, navigation, source links, clipboard writes, actual replay-recorder exclusion, themes, widths, and sign-out. CI provisions this isolated environment with `next dev`; the main checks workflow separately validates the production build. This keeps the disposable extension origin and identity independent of the shared app build. Set `EXTENSION_APP_E2E_LIVE_CHAT=1` when the app is connected to a working local Mothership instance to additionally verify live rendering, saved page context, queue editing, and drafts through chat admission. Provider credentials belong only in that server's environment. Reports and screenshots are written beside the supplied report path. Native toolbar/shortcut/context-menu and permission-prompt checks remain manual release checks.
61+
Configure the matching extension ID in the running app. The suite builds into a temporary directory, creates its own accounts/workspaces, and removes them afterward; it leaves the shipping `dist` untouched. It verifies real authentication, workspace denial, page limits, navigation, source links, clipboard writes, actual replay-recorder exclusion, themes, widths, and sign-out. CI provisions this isolated environment in the shared HTTP acceptance matrix, using the same production build as the other app suites. The packaged extension transport suite runs separately. Set `EXTENSION_APP_E2E_LIVE_CHAT=1` when the app is connected to a working local Mothership instance to additionally verify live rendering, saved page context, queue editing, and drafts through chat admission. Provider credentials belong only in that server's environment. Reports and screenshots are written beside the supplied report path. Native toolbar/shortcut/context-menu and permission-prompt checks remain manual release checks.
6262

6363
## Distribution
6464

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,20 @@ try {
258258
)
259259
await page.reload()
260260
})
261+
await check(
262+
'An expired toolbar intent asks for a fresh capture without delivering content',
263+
async () => {
264+
const before = (await snapshots()).length
265+
const intent = { ...(await pendingIntent()), createdAt: Date.now() - 60_000 }
266+
await worker.evaluate(async (pending) => {
267+
await chrome.storage.session.set({ [`captureIntent:${pending.windowId}:expired`]: pending })
268+
}, intent)
269+
await expect(panel.getByRole('status')).toHaveText(
270+
'The capture expired. Attach the page again.'
271+
)
272+
assert((await snapshots()).length === before, 'Expired toolbar intent delivered content')
273+
}
274+
)
261275
await check('Page extraction excludes secrets and strips URL query and fragment', async () => {
262276
const [snapshot] = await snapshots()
263277
assert(snapshot.text.includes('visible-reference-482'), 'Visible page content missing')

‎apps/extension/src/sidepanel.tsx‎

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,12 @@ function SidePanel({ origin }: SidePanelProps) {
152152
)
153153
return
154154
}
155-
if (!disposed && intent && Date.now() - intent.createdAt <= CAPTURE_INTENT_TTL_MS)
156-
await capture(intent.kind, intent)
155+
if (disposed || !intent) return
156+
if (Date.now() - intent.createdAt > CAPTURE_INTENT_TTL_MS) {
157+
setStatus('The capture expired. Attach the page again.')
158+
return
159+
}
160+
await capture(intent.kind, intent)
157161
} catch {
158162
if (!disposed) setStatus('Could not receive the capture request. Attach the page again.')
159163
} finally {
@@ -209,10 +213,18 @@ function SidePanel({ origin }: SidePanelProps) {
209213
}
210214
void consumeIntent()
211215
}, 5_000)
216+
const tabUpdated = (
217+
_tabId: number,
218+
change: chrome.tabs.OnUpdatedInfo,
219+
tab: chrome.tabs.Tab
220+
) => {
221+
if (!tab.active || (change.url === undefined && change.status === undefined)) return
222+
update()
223+
}
212224
window.addEventListener('message', message)
213225
chrome.storage.onChanged.addListener(storageChanged)
214226
chrome.tabs.onActivated.addListener(update)
215-
chrome.tabs.onUpdated.addListener(update)
227+
chrome.tabs.onUpdated.addListener(tabUpdated)
216228
chrome.permissions.onAdded.addListener(update)
217229
chrome.permissions.onRemoved.addListener(update)
218230
update()
@@ -227,7 +239,7 @@ function SidePanel({ origin }: SidePanelProps) {
227239
window.removeEventListener('message', message)
228240
chrome.storage.onChanged.removeListener(storageChanged)
229241
chrome.tabs.onActivated.removeListener(update)
230-
chrome.tabs.onUpdated.removeListener(update)
242+
chrome.tabs.onUpdated.removeListener(tabUpdated)
231243
chrome.permissions.onAdded.removeListener(update)
232244
chrome.permissions.onRemoved.removeListener(update)
233245
}
@@ -271,9 +283,8 @@ function SidePanel({ origin }: SidePanelProps) {
271283
<ChipModalHeader onClose={() => setAccessOpen(false)}>Site access</ChipModalHeader>
272284
<ChipModalBody>
273285
{site ? (
274-
<Chip onClick={() => void toggleSite()}>
275-
{site.allowed ? 'Remove access to' : 'Allow access to'}{' '}
276-
{new URL(site.origin).hostname}
286+
<Chip className='max-w-full' onClick={() => void toggleSite()}>
287+
{`${site.allowed ? 'Remove access to' : 'Allow access to'} ${new URL(site.origin).hostname}`}
277288
</Chip>
278289
) : (
279290
<p className='text-[var(--text-muted)] text-small'>

0 commit comments

Comments
 (0)