Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
5a7b924
feat(audit): record the client surface on audit entries and identify …
waleedlatif1 Sep 14, 2026
ad05dae
fix(agent): retain conversation memory attachments (#7818)
icecrasher321 Sep 14, 2026
0734cd0
feat(slack): switch custom bots to the Sim Search app (#7811)
TheodoreSpeaks Sep 14, 2026
b2b9eed
fix(tables): use explicit timestamps for expiration (#7689)
j15z Sep 14, 2026
b49d28f
fix(chat): render deployed file outputs inline (#7826)
icecrasher321 Sep 14, 2026
7730fd3
fix(desktop): unify shell dialogs and recovery screens with emcn (#7828)
waleedlatif1 Sep 14, 2026
8a4a0e1
improvement(search): speed up organization integration and source que…
waleedlatif1 Sep 14, 2026
4ef52fd
fix(confluence): drop scripts, styles, and unresolved Jira placeholde…
waleedlatif1 Sep 14, 2026
95e0607
feat(agent): allow variable tool permission modes (#7538)
j15z Sep 15, 2026
580fc58
fix(search): bound retrieval and resume projection upgrades (#7824)
icecrasher321 Sep 15, 2026
4460a0a
feat(slack): support organization-scoped shared app rollout (#7833)
TheodoreSpeaks Sep 15, 2026
89a8b12
improvement(analytics): attribute every request, credential, and queu…
waleedlatif1 Sep 15, 2026
2e08162
improvement(editor): render tool permission mode and retry fields wit…
waleedlatif1 Sep 15, 2026
55f881f
docs(agent): document the variable tool Permission Mode (#7832)
j15z Sep 15, 2026
058c37b
fix(db): allow five hours for database migrations (#7838)
icecrasher321 Sep 15, 2026
c3f8140
feat(sso): let an organization keep several identity providers on one…
waleedlatif1 Sep 15, 2026
fb92772
improvement(desktop): remove per-task website approval prompts (#7837)
waleedlatif1 Sep 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
4 changes: 4 additions & 0 deletions .github/workflows/desktop-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ on:
- '.github/workflows/desktop-e2e.yml'
- '.github/workflows/desktop-release.yml'
- 'apps/desktop/**'
- 'apps/sim/app/_styles/**'
- 'apps/sim/lib/postcss/**'
- 'apps/sim/postcss.config.mjs'
- 'apps/sim/public/brand/fonts/**'
- 'packages/emcn/**'
- 'packages/desktop-bridge/**'
- 'packages/browser-protocol/**'
- 'packages/terminal-protocol/**'
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/migrations.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ jobs:
migrate:
name: Apply Database Migrations
runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }}
timeout-minutes: 45
# Bulk projection loads and concurrent index builds can outlast ordinary schema changes.
timeout-minutes: 300

steps:
- name: Checkout code
Expand Down
11 changes: 10 additions & 1 deletion .github/workflows/test-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ jobs:
lib/auth/sim-auth-adapter.postgres.test.ts
ee/scim/lib/managed-membership.postgres.test.ts
lib/auth/sso/application/admit-sso-user.postgres.test.ts
lib/auth/sso/primary-provider.postgres.test.ts

- name: Verify cumulative billing timeout recovery in PostgreSQL
working-directory: apps/sim
Expand Down Expand Up @@ -170,15 +171,23 @@ jobs:
if-no-files-found: ignore
retention-days: 7

- name: Verify durable provenance bindings and concurrent memory writes
- name: Verify durable provenance, concurrent memory writes, and attachment replay
working-directory: apps/sim
env:
TABLE_PROVENANCE_TEST_DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/sim_auth_scim
MEMORY_PROVENANCE_TEST_DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/sim_auth_scim
AGENT_MEMORY_TEST_DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/sim_auth_scim
run: >-
bunx vitest run
lib/table/rows/secret-provenance.postgres.test.ts
lib/memory/message-provenance.postgres.test.ts
executor/handlers/agent/memory-harness.postgres.test.ts

- name: Verify Search vector projection upgrade in PostgreSQL
working-directory: packages/db
env:
KNOWLEDGE_ACL_TEST_DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/sim_auth_scim
run: bunx vitest run script-migrations/0016_backfill_search_vectors.postgres.test.ts

- name: Verify Search progress, pagination, and outbox scheduling in PostgreSQL
working-directory: apps/sim
Expand Down
28 changes: 28 additions & 0 deletions apps/desktop/e2e/browser-tools.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const FORM = `<!doctype html><html><head><title>Form fixture</title></head><body
<label>Updates <input id="updates" type="checkbox"></label>
<label>Password <input id="password" type="password"></label>
<label>Route <input id="route" oninput="history.pushState({}, '', '/form?changed=1')"></label>
<a href="/redirect">Other website</a>
<div id="horizontal" role="region" aria-label="Wide table" tabindex="0" style="width:280px;overflow-x:auto">
<div style="width:1600px;height:100px">Wide content</div>
</div>
Expand All @@ -40,6 +41,11 @@ test.describe('browser tools', () => {
test.beforeAll(async () => {
server = createServer(async (request, response) => {
const path = new URL(request.url ?? '/', 'http://127.0.0.1').pathname
if (path === '/redirect') {
response.writeHead(302, { Location: `${origin.replace('127.0.0.1', 'localhost')}/landing` })
response.end()
return
}
if (path === '/api/desktop/tool/authorize') {
let body = ''
for await (const chunk of request) body += chunk.toString()
Expand Down Expand Up @@ -189,6 +195,28 @@ test.describe('browser tools', () => {
expect(await formState()).toMatchObject({ name: '', route: 'change route' })
})

test('follows a link and cross-origin redirect without a website approval prompt', async () => {
await openForm()
await app.evaluate(async ({ webContents }, url) => {
const page = webContents.getAllWebContents().find((contents) => contents.getURL() === url)
if (!page) throw new Error('Missing browser fixture')
await page.executeJavaScript("document.querySelector('a').click()")
}, `${origin}/form`)

const destination = `${origin.replace('127.0.0.1', 'localhost')}/landing`
await expect
.poll(() =>
app.evaluate(
({ webContents }, url) =>
webContents.getAllWebContents().some((contents) => contents.getURL() === url),
destination
)
)
.toBe(true)
expect(await app.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows().length)).toBe(1)
await expect(window.getByRole('heading')).toHaveText('Browser tools fixture')
})

test('stops when a new popup exceeds the page summary limit', async () => {
const ref = await openForm()
await app.evaluate(async ({ webContents }, origin) => {
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/e2e/packaged-smoke.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,8 @@ test('packaged shell renders the bundled offline page', async () => {
.toBe(true)
const picker = findPage('sim-shell://pages/server.html')
if (!picker) throw new Error('server picker disappeared')
await expect(picker.locator('h1')).toHaveText('Sim server')
await expect(picker.locator('#origin')).toHaveValue('http://127.0.0.1:1')
await expect(picker.getByRole('dialog', { name: 'Sim server' })).toBeVisible()
await expect(picker.getByLabel('Server URL')).toHaveValue('http://127.0.0.1:1')
} finally {
await browser?.close().catch(() => {})
if (child.exitCode === null && child.signalCode === null) {
Expand Down
115 changes: 102 additions & 13 deletions apps/desktop/e2e/smoke.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,11 @@ test.describe('desktop shell smoke', () => {
const window = await app.firstWindow()
await window.waitForSelector('#retry', { timeout: 30_000 })
expect(window.url()).toMatch(/^sim-shell:\/\/pages\/offline\.html\?/)
await expect(window.locator('.wordmark')).toBeVisible()
await expect(window.locator('.wordmark')).toHaveAttribute('aria-label', 'Sim')
await expect(window.getByRole('img', { name: 'Sim', exact: true })).toBeVisible()
await expect(window.getByRole('img', { name: 'Sim', exact: true })).toHaveAttribute(
'aria-label',
'Sim'
)
await expect(window.locator('#title')).toHaveText('Can’t connect to Sim')
// The recovery path for a self-hosted shell pointed at a server it cannot
// reach. Exercised end to end here because it is the only coverage of the
Expand All @@ -173,22 +176,74 @@ test.describe('desktop shell smoke', () => {
await expect
.poll(() => window.evaluate(() => document.fonts.check('16px "Season Sans"')))
.toBe(true)
await expect(window.locator('#retry')).toHaveCSS('height', '30px')
await expect(window.locator('#retry')).toHaveCSS('border-radius', '8px')
await expect(window.locator('#retry')).toHaveCSS('padding-left', '8px')
await expect(window.locator('#retry')).toHaveCSS('font-size', '14px')
await expect(window.locator('#retry')).toHaveCSS('line-height', '20px')
await expect(window.locator('#retry')).toHaveCSS('text-align', 'left')
await window.locator('#retry').focus()
await expect(window.locator('#retry')).toHaveCSS('outline-style', 'solid')
await expect(window.locator('#detail')).toHaveAttribute('role', 'status')
})

test('recovery messages use an isolated EMCN dialog with a safe keyboard default', async () => {
app = await launchApp('http://127.0.0.1:1')
const window = await app.firstWindow()
await expect(window.locator('#server')).toBeVisible()
const dialogPromise = app.waitForEvent('window')
await app.evaluate(({ BrowserWindow }) => {
BrowserWindow.getAllWindows()[0].webContents.emit('unresponsive')
})
const prompt = await dialogPromise
await expect(prompt.getByRole('dialog', { name: 'Sim', exact: true })).toBeVisible()
await expect(prompt.getByText('Sim isn’t responding')).toBeVisible()
await expect(prompt.getByRole('button', { name: 'Wait', exact: true })).toBeFocused()
await expect
.poll(() =>
prompt
.getByRole('dialog')
.evaluate((element) => element.scrollHeight <= globalThis.innerHeight)
)
.toBe(true)
await expect
.poll(() => prompt.evaluate(() => typeof (globalThis as { simDesktop?: unknown }).simDesktop))
.toBe('undefined')
await prompt.screenshot({
path: test.info().outputPath('recovery-dialog.png'),
animations: 'disabled',
})
await app.evaluate(({ BrowserWindow }) => {
const win = BrowserWindow.getAllWindows().find(
(entry) => entry.webContents.getURL() === 'sim-shell://pages/dialog.html'
)
if (!win) throw new Error('Recovery dialog is missing')
win.webContents.ipc.removeHandler('shell:configuration')
win.webContents.ipc.handle('shell:configuration', () => ({
title: 'Long recovery message',
message: 'Recovery details',
detail: Array.from({ length: 80 }, (_, index) => `Diagnostic detail ${index + 1}`).join(
'\n'
),
type: 'warning',
buttons: ['Wait', 'Reload'],
defaultId: 0,
cancelId: 0,
}))
win.webContents.reload()
})
await expect(
prompt.getByRole('dialog', { name: 'Long recovery message', exact: true })
).toBeVisible()
await expect(prompt.getByRole('button', { name: 'Reload', exact: true })).toBeInViewport()
await expect(prompt.getByRole('button', { name: 'Wait', exact: true })).toBeFocused()
const closed = prompt.waitForEvent('close')
await prompt
.getByRole('button', { name: 'Wait', exact: true })
.press('Enter')
.catch(() => {})
await closed
await expect(window.locator('#server')).toBeVisible()
})

// The picker is the only way to repoint a shell whose server is unreachable.
// Its page, the pre-filled value (which crosses the local-page IPC gate) and
// Escape are asserted together because the packaged build once opened it as
// a blank sheet with no way out.
test('the offline page opens the server picker, pre-filled, and Escape closes it', async () => {
test('the offline server picker renders EMCN controls and handles validation and dismissal', async () => {
const testInfo = test.info()
app = await launchApp('http://127.0.0.1:1')
const window = await app.firstWindow()
await window.waitForSelector('#server', { timeout: 30_000 })
Expand All @@ -198,8 +253,42 @@ test.describe('desktop shell smoke', () => {
const picker = await pickerPromise

expect(picker.url()).toBe('sim-shell://pages/server.html')
await expect(picker.locator('h1')).toHaveText('Sim server')
await expect(picker.locator('#origin')).toHaveValue('http://127.0.0.1:1')
await expect(picker.getByRole('dialog', { name: 'Sim server', exact: true })).toBeVisible()
await expect(picker.getByLabel('Server URL')).toHaveValue('http://127.0.0.1:1')
await expect(picker.getByLabel('Server URL')).toBeFocused()
await expect
.poll(() =>
picker
.getByRole('dialog')
.evaluate((element) => element.scrollHeight <= globalThis.innerHeight)
)
.toBe(true)
await picker.getByLabel('Server URL').fill('http://example.com')
await picker.getByLabel('Server URL').press('Enter')
await expect(picker.getByRole('alert')).toBeVisible()
await expect(picker.getByLabel('Server URL')).toHaveAttribute('aria-invalid', 'true')
await picker.getByLabel('Server URL').fill('http://127.0.0.1:1')
await expect(picker.getByRole('alert')).toHaveCount(0)
await picker.getByRole('button', { name: 'Connect', exact: true }).click()
await expect(picker.getByRole('status')).toHaveText('Already connected to this server.')
await expect
.poll(() =>
picker
.locator('[data-chip-modal-body]')
.evaluate((element) => element.scrollHeight <= element.clientHeight)
)
.toBe(true)
await picker.emulateMedia({ colorScheme: 'light' })
await picker.screenshot({
path: testInfo.outputPath('server-modal-light.png'),
animations: 'disabled',
})
await picker.emulateMedia({ colorScheme: 'dark' })
await expect(picker.locator('html')).toHaveClass('dark')
await picker.screenshot({
path: testInfo.outputPath('server-modal-dark.png'),
animations: 'disabled',
})

const closed = picker.waitForEvent('close')
// The main process destroys the window on the key-down, so the key-up half
Expand Down
9 changes: 8 additions & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "0.0.0",
"private": true,
"license": "Apache-2.0",
"description": "Sim desktop app for macOS Electron shell around the hosted web app",
"description": "Sim desktop app for macOS \u2014 Electron shell around the hosted web app",
"author": "Sim <support@sim.ai>",
"homepage": "https://sim.ai",
"type": "module",
Expand Down Expand Up @@ -47,13 +47,20 @@
"devDependencies": {
"@electron/fuses": "1.8.0",
"@playwright/test": "1.61.1",
"@sim/emcn": "workspace:*",
"@sim/tsconfig": "workspace:*",
"@types/micromatch": "4.0.10",
"@types/node": "24.2.1",
"@types/react": "^19",
"@types/react-dom": "^19",
"electron": "43.5.0",
"electron-builder": "26.15.3",
"esbuild": "0.28.1",
"jsdom": "^26.0.0",
"postcss": "^8",
"postcss-load-config": "6.0.1",
"react": "19.2.4",
"react-dom": "19.2.4",
"typescript": "^7.0.2",
"vitest": "^4.1.0"
}
Expand Down
64 changes: 60 additions & 4 deletions apps/desktop/scripts/build.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { execFileSync } from 'node:child_process'
import { cpSync, existsSync, mkdirSync, rmSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { build } from 'esbuild'
import { cpSync, existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs'
import { dirname, join, resolve } from 'node:path'
import { type BuildOptions, build } from 'esbuild'
import postcss from 'postcss'
import loadPostcssConfig from 'postcss-load-config'
import { identityForOrigin } from './channels'

const watch = process.argv.includes('--watch')
Expand Down Expand Up @@ -94,10 +96,56 @@ const common = {
},
}

/** Bundles the shared EMCN components and app tokens for offline shell use. */
const renderer: BuildOptions = {
entryPoints: {
server: 'src/renderer/server/index.tsx',
offline: 'src/renderer/offline/index.tsx',
dialog: 'src/renderer/dialog/index.tsx',
},
outdir: 'dist/renderer',
bundle: true,
platform: 'browser',
format: 'iife',
target: 'chrome146',
minify: true,
tsconfig: 'tsconfig.json',
external: ['*.woff2'],
define: { 'process.env.NODE_ENV': '"production"', 'process.env': '{}' },
loader: { '.module.css': 'local-css' },
plugins: [
{
name: 'desktop-tailwind',
setup(builder) {
builder.onLoad({ filter: /shell\.css$/ }, async ({ path }) => {
const config = await loadPostcssConfig({}, resolve('../sim'))
const result = await postcss(config.plugins).process(readFileSync(path, 'utf8'), {
from: path,
})
return {
contents: result.css,
loader: 'css',
resolveDir: dirname(path),
watchFiles: result.messages.flatMap((message) =>
message.type === 'dependency' ? [message.file as string] : []
),
}
})
},
},
],
}

async function run(): Promise<void> {
compileNativeHelpSearch()
if (watch) {
const { context } = await import('esbuild')
const rendererCtx = await context(renderer)
const shellPreloadCtx = await context({
...common,
entryPoints: ['src/preload/shell.ts'],
outfile: 'dist/shell-preload.cjs',
})
const mainCtx = await context({
...common,
entryPoints: ['src/main/index.ts'],
Expand All @@ -115,10 +163,18 @@ async function run(): Promise<void> {
entryPoints: ['src/preload/browser/index.ts'],
outfile: 'dist/browser-preload.cjs',
})
await Promise.all([mainCtx.watch(), preloadCtx.watch(), browserPreloadCtx.watch()])
await Promise.all([
mainCtx.watch(),
preloadCtx.watch(),
browserPreloadCtx.watch(),
rendererCtx.watch(),
shellPreloadCtx.watch(),
])
return
}
await Promise.all([
build(renderer),
build({ ...common, entryPoints: ['src/preload/shell.ts'], outfile: 'dist/shell-preload.cjs' }),
build({ ...common, entryPoints: ['src/main/index.ts'], outfile: 'dist/main.cjs' }),
build({ ...common, entryPoints: ['src/preload/index.ts'], outfile: 'dist/preload.cjs' }),
build({
Expand Down
Loading
Loading