Skip to content

Commit 6781650

Browse files
authored
v0.8.37: cli telemetry enhancement, slack search, search speedup, org multiple sso provider support, confluence connector improvements
2 parents 2b139e4 + fb92772 commit 6781650

323 files changed

Lines changed: 119119 additions & 4041 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/workflows/desktop-e2e.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ on:
1010
- '.github/workflows/desktop-e2e.yml'
1111
- '.github/workflows/desktop-release.yml'
1212
- 'apps/desktop/**'
13+
- 'apps/sim/app/_styles/**'
14+
- 'apps/sim/lib/postcss/**'
15+
- 'apps/sim/postcss.config.mjs'
1316
- 'apps/sim/public/brand/fonts/**'
17+
- 'packages/emcn/**'
1418
- 'packages/desktop-bridge/**'
1519
- 'packages/browser-protocol/**'
1620
- 'packages/terminal-protocol/**'

.github/workflows/migrations.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ jobs:
2525
migrate:
2626
name: Apply Database Migrations
2727
runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }}
28-
timeout-minutes: 45
28+
# Bulk projection loads and concurrent index builds can outlast ordinary schema changes.
29+
timeout-minutes: 300
2930

3031
steps:
3132
- name: Checkout code

.github/workflows/test-build.yml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ jobs:
9696
lib/auth/sim-auth-adapter.postgres.test.ts
9797
ee/scim/lib/managed-membership.postgres.test.ts
9898
lib/auth/sso/application/admit-sso-user.postgres.test.ts
99+
lib/auth/sso/primary-provider.postgres.test.ts
99100
100101
- name: Verify cumulative billing timeout recovery in PostgreSQL
101102
working-directory: apps/sim
@@ -170,15 +171,23 @@ jobs:
170171
if-no-files-found: ignore
171172
retention-days: 7
172173

173-
- name: Verify durable provenance bindings and concurrent memory writes
174+
- name: Verify durable provenance, concurrent memory writes, and attachment replay
174175
working-directory: apps/sim
175176
env:
176177
TABLE_PROVENANCE_TEST_DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/sim_auth_scim
177178
MEMORY_PROVENANCE_TEST_DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/sim_auth_scim
179+
AGENT_MEMORY_TEST_DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/sim_auth_scim
178180
run: >-
179181
bunx vitest run
180182
lib/table/rows/secret-provenance.postgres.test.ts
181183
lib/memory/message-provenance.postgres.test.ts
184+
executor/handlers/agent/memory-harness.postgres.test.ts
185+
186+
- name: Verify Search vector projection upgrade in PostgreSQL
187+
working-directory: packages/db
188+
env:
189+
KNOWLEDGE_ACL_TEST_DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/sim_auth_scim
190+
run: bunx vitest run script-migrations/0016_backfill_search_vectors.postgres.test.ts
182191

183192
- name: Verify Search progress, pagination, and outbox scheduling in PostgreSQL
184193
working-directory: apps/sim

apps/desktop/e2e/browser-tools.spec.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const FORM = `<!doctype html><html><head><title>Form fixture</title></head><body
2121
<label>Updates <input id="updates" type="checkbox"></label>
2222
<label>Password <input id="password" type="password"></label>
2323
<label>Route <input id="route" oninput="history.pushState({}, '', '/form?changed=1')"></label>
24+
<a href="/redirect">Other website</a>
2425
<div id="horizontal" role="region" aria-label="Wide table" tabindex="0" style="width:280px;overflow-x:auto">
2526
<div style="width:1600px;height:100px">Wide content</div>
2627
</div>
@@ -40,6 +41,11 @@ test.describe('browser tools', () => {
4041
test.beforeAll(async () => {
4142
server = createServer(async (request, response) => {
4243
const path = new URL(request.url ?? '/', 'http://127.0.0.1').pathname
44+
if (path === '/redirect') {
45+
response.writeHead(302, { Location: `${origin.replace('127.0.0.1', 'localhost')}/landing` })
46+
response.end()
47+
return
48+
}
4349
if (path === '/api/desktop/tool/authorize') {
4450
let body = ''
4551
for await (const chunk of request) body += chunk.toString()
@@ -189,6 +195,28 @@ test.describe('browser tools', () => {
189195
expect(await formState()).toMatchObject({ name: '', route: 'change route' })
190196
})
191197

198+
test('follows a link and cross-origin redirect without a website approval prompt', async () => {
199+
await openForm()
200+
await app.evaluate(async ({ webContents }, url) => {
201+
const page = webContents.getAllWebContents().find((contents) => contents.getURL() === url)
202+
if (!page) throw new Error('Missing browser fixture')
203+
await page.executeJavaScript("document.querySelector('a').click()")
204+
}, `${origin}/form`)
205+
206+
const destination = `${origin.replace('127.0.0.1', 'localhost')}/landing`
207+
await expect
208+
.poll(() =>
209+
app.evaluate(
210+
({ webContents }, url) =>
211+
webContents.getAllWebContents().some((contents) => contents.getURL() === url),
212+
destination
213+
)
214+
)
215+
.toBe(true)
216+
expect(await app.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows().length)).toBe(1)
217+
await expect(window.getByRole('heading')).toHaveText('Browser tools fixture')
218+
})
219+
192220
test('stops when a new popup exceeds the page summary limit', async () => {
193221
const ref = await openForm()
194222
await app.evaluate(async ({ webContents }, origin) => {

apps/desktop/e2e/packaged-smoke.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,8 @@ test('packaged shell renders the bundled offline page', async () => {
146146
.toBe(true)
147147
const picker = findPage('sim-shell://pages/server.html')
148148
if (!picker) throw new Error('server picker disappeared')
149-
await expect(picker.locator('h1')).toHaveText('Sim server')
150-
await expect(picker.locator('#origin')).toHaveValue('http://127.0.0.1:1')
149+
await expect(picker.getByRole('dialog', { name: 'Sim server' })).toBeVisible()
150+
await expect(picker.getByLabel('Server URL')).toHaveValue('http://127.0.0.1:1')
151151
} finally {
152152
await browser?.close().catch(() => {})
153153
if (child.exitCode === null && child.signalCode === null) {

apps/desktop/e2e/smoke.spec.ts

Lines changed: 102 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -158,8 +158,11 @@ test.describe('desktop shell smoke', () => {
158158
const window = await app.firstWindow()
159159
await window.waitForSelector('#retry', { timeout: 30_000 })
160160
expect(window.url()).toMatch(/^sim-shell:\/\/pages\/offline\.html\?/)
161-
await expect(window.locator('.wordmark')).toBeVisible()
162-
await expect(window.locator('.wordmark')).toHaveAttribute('aria-label', 'Sim')
161+
await expect(window.getByRole('img', { name: 'Sim', exact: true })).toBeVisible()
162+
await expect(window.getByRole('img', { name: 'Sim', exact: true })).toHaveAttribute(
163+
'aria-label',
164+
'Sim'
165+
)
163166
await expect(window.locator('#title')).toHaveText('Can’t connect to Sim')
164167
// The recovery path for a self-hosted shell pointed at a server it cannot
165168
// reach. Exercised end to end here because it is the only coverage of the
@@ -173,22 +176,74 @@ test.describe('desktop shell smoke', () => {
173176
await expect
174177
.poll(() => window.evaluate(() => document.fonts.check('16px "Season Sans"')))
175178
.toBe(true)
176-
await expect(window.locator('#retry')).toHaveCSS('height', '30px')
177-
await expect(window.locator('#retry')).toHaveCSS('border-radius', '8px')
178-
await expect(window.locator('#retry')).toHaveCSS('padding-left', '8px')
179-
await expect(window.locator('#retry')).toHaveCSS('font-size', '14px')
180-
await expect(window.locator('#retry')).toHaveCSS('line-height', '20px')
181-
await expect(window.locator('#retry')).toHaveCSS('text-align', 'left')
182-
await window.locator('#retry').focus()
183-
await expect(window.locator('#retry')).toHaveCSS('outline-style', 'solid')
184179
await expect(window.locator('#detail')).toHaveAttribute('role', 'status')
185180
})
186181

182+
test('recovery messages use an isolated EMCN dialog with a safe keyboard default', async () => {
183+
app = await launchApp('http://127.0.0.1:1')
184+
const window = await app.firstWindow()
185+
await expect(window.locator('#server')).toBeVisible()
186+
const dialogPromise = app.waitForEvent('window')
187+
await app.evaluate(({ BrowserWindow }) => {
188+
BrowserWindow.getAllWindows()[0].webContents.emit('unresponsive')
189+
})
190+
const prompt = await dialogPromise
191+
await expect(prompt.getByRole('dialog', { name: 'Sim', exact: true })).toBeVisible()
192+
await expect(prompt.getByText('Sim isn’t responding')).toBeVisible()
193+
await expect(prompt.getByRole('button', { name: 'Wait', exact: true })).toBeFocused()
194+
await expect
195+
.poll(() =>
196+
prompt
197+
.getByRole('dialog')
198+
.evaluate((element) => element.scrollHeight <= globalThis.innerHeight)
199+
)
200+
.toBe(true)
201+
await expect
202+
.poll(() => prompt.evaluate(() => typeof (globalThis as { simDesktop?: unknown }).simDesktop))
203+
.toBe('undefined')
204+
await prompt.screenshot({
205+
path: test.info().outputPath('recovery-dialog.png'),
206+
animations: 'disabled',
207+
})
208+
await app.evaluate(({ BrowserWindow }) => {
209+
const win = BrowserWindow.getAllWindows().find(
210+
(entry) => entry.webContents.getURL() === 'sim-shell://pages/dialog.html'
211+
)
212+
if (!win) throw new Error('Recovery dialog is missing')
213+
win.webContents.ipc.removeHandler('shell:configuration')
214+
win.webContents.ipc.handle('shell:configuration', () => ({
215+
title: 'Long recovery message',
216+
message: 'Recovery details',
217+
detail: Array.from({ length: 80 }, (_, index) => `Diagnostic detail ${index + 1}`).join(
218+
'\n'
219+
),
220+
type: 'warning',
221+
buttons: ['Wait', 'Reload'],
222+
defaultId: 0,
223+
cancelId: 0,
224+
}))
225+
win.webContents.reload()
226+
})
227+
await expect(
228+
prompt.getByRole('dialog', { name: 'Long recovery message', exact: true })
229+
).toBeVisible()
230+
await expect(prompt.getByRole('button', { name: 'Reload', exact: true })).toBeInViewport()
231+
await expect(prompt.getByRole('button', { name: 'Wait', exact: true })).toBeFocused()
232+
const closed = prompt.waitForEvent('close')
233+
await prompt
234+
.getByRole('button', { name: 'Wait', exact: true })
235+
.press('Enter')
236+
.catch(() => {})
237+
await closed
238+
await expect(window.locator('#server')).toBeVisible()
239+
})
240+
187241
// The picker is the only way to repoint a shell whose server is unreachable.
188242
// Its page, the pre-filled value (which crosses the local-page IPC gate) and
189243
// Escape are asserted together because the packaged build once opened it as
190244
// a blank sheet with no way out.
191-
test('the offline page opens the server picker, pre-filled, and Escape closes it', async () => {
245+
test('the offline server picker renders EMCN controls and handles validation and dismissal', async () => {
246+
const testInfo = test.info()
192247
app = await launchApp('http://127.0.0.1:1')
193248
const window = await app.firstWindow()
194249
await window.waitForSelector('#server', { timeout: 30_000 })
@@ -198,8 +253,42 @@ test.describe('desktop shell smoke', () => {
198253
const picker = await pickerPromise
199254

200255
expect(picker.url()).toBe('sim-shell://pages/server.html')
201-
await expect(picker.locator('h1')).toHaveText('Sim server')
202-
await expect(picker.locator('#origin')).toHaveValue('http://127.0.0.1:1')
256+
await expect(picker.getByRole('dialog', { name: 'Sim server', exact: true })).toBeVisible()
257+
await expect(picker.getByLabel('Server URL')).toHaveValue('http://127.0.0.1:1')
258+
await expect(picker.getByLabel('Server URL')).toBeFocused()
259+
await expect
260+
.poll(() =>
261+
picker
262+
.getByRole('dialog')
263+
.evaluate((element) => element.scrollHeight <= globalThis.innerHeight)
264+
)
265+
.toBe(true)
266+
await picker.getByLabel('Server URL').fill('http://example.com')
267+
await picker.getByLabel('Server URL').press('Enter')
268+
await expect(picker.getByRole('alert')).toBeVisible()
269+
await expect(picker.getByLabel('Server URL')).toHaveAttribute('aria-invalid', 'true')
270+
await picker.getByLabel('Server URL').fill('http://127.0.0.1:1')
271+
await expect(picker.getByRole('alert')).toHaveCount(0)
272+
await picker.getByRole('button', { name: 'Connect', exact: true }).click()
273+
await expect(picker.getByRole('status')).toHaveText('Already connected to this server.')
274+
await expect
275+
.poll(() =>
276+
picker
277+
.locator('[data-chip-modal-body]')
278+
.evaluate((element) => element.scrollHeight <= element.clientHeight)
279+
)
280+
.toBe(true)
281+
await picker.emulateMedia({ colorScheme: 'light' })
282+
await picker.screenshot({
283+
path: testInfo.outputPath('server-modal-light.png'),
284+
animations: 'disabled',
285+
})
286+
await picker.emulateMedia({ colorScheme: 'dark' })
287+
await expect(picker.locator('html')).toHaveClass('dark')
288+
await picker.screenshot({
289+
path: testInfo.outputPath('server-modal-dark.png'),
290+
animations: 'disabled',
291+
})
203292

204293
const closed = picker.waitForEvent('close')
205294
// The main process destroys the window on the key-down, so the key-up half

apps/desktop/package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"version": "0.0.0",
44
"private": true,
55
"license": "Apache-2.0",
6-
"description": "Sim desktop app for macOS Electron shell around the hosted web app",
6+
"description": "Sim desktop app for macOS \u2014 Electron shell around the hosted web app",
77
"author": "Sim <support@sim.ai>",
88
"homepage": "https://sim.ai",
99
"type": "module",
@@ -47,13 +47,20 @@
4747
"devDependencies": {
4848
"@electron/fuses": "1.8.0",
4949
"@playwright/test": "1.61.1",
50+
"@sim/emcn": "workspace:*",
5051
"@sim/tsconfig": "workspace:*",
5152
"@types/micromatch": "4.0.10",
5253
"@types/node": "24.2.1",
54+
"@types/react": "^19",
55+
"@types/react-dom": "^19",
5356
"electron": "43.5.0",
5457
"electron-builder": "26.15.3",
5558
"esbuild": "0.28.1",
5659
"jsdom": "^26.0.0",
60+
"postcss": "^8",
61+
"postcss-load-config": "6.0.1",
62+
"react": "19.2.4",
63+
"react-dom": "19.2.4",
5764
"typescript": "^7.0.2",
5865
"vitest": "^4.1.0"
5966
}

apps/desktop/scripts/build.ts

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { execFileSync } from 'node:child_process'
2-
import { cpSync, existsSync, mkdirSync, rmSync } from 'node:fs'
3-
import { dirname, join } from 'node:path'
4-
import { build } from 'esbuild'
2+
import { cpSync, existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs'
3+
import { dirname, join, resolve } from 'node:path'
4+
import { type BuildOptions, build } from 'esbuild'
5+
import postcss from 'postcss'
6+
import loadPostcssConfig from 'postcss-load-config'
57
import { identityForOrigin } from './channels'
68

79
const watch = process.argv.includes('--watch')
@@ -94,10 +96,56 @@ const common = {
9496
},
9597
}
9698

99+
/** Bundles the shared EMCN components and app tokens for offline shell use. */
100+
const renderer: BuildOptions = {
101+
entryPoints: {
102+
server: 'src/renderer/server/index.tsx',
103+
offline: 'src/renderer/offline/index.tsx',
104+
dialog: 'src/renderer/dialog/index.tsx',
105+
},
106+
outdir: 'dist/renderer',
107+
bundle: true,
108+
platform: 'browser',
109+
format: 'iife',
110+
target: 'chrome146',
111+
minify: true,
112+
tsconfig: 'tsconfig.json',
113+
external: ['*.woff2'],
114+
define: { 'process.env.NODE_ENV': '"production"', 'process.env': '{}' },
115+
loader: { '.module.css': 'local-css' },
116+
plugins: [
117+
{
118+
name: 'desktop-tailwind',
119+
setup(builder) {
120+
builder.onLoad({ filter: /shell\.css$/ }, async ({ path }) => {
121+
const config = await loadPostcssConfig({}, resolve('../sim'))
122+
const result = await postcss(config.plugins).process(readFileSync(path, 'utf8'), {
123+
from: path,
124+
})
125+
return {
126+
contents: result.css,
127+
loader: 'css',
128+
resolveDir: dirname(path),
129+
watchFiles: result.messages.flatMap((message) =>
130+
message.type === 'dependency' ? [message.file as string] : []
131+
),
132+
}
133+
})
134+
},
135+
},
136+
],
137+
}
138+
97139
async function run(): Promise<void> {
98140
compileNativeHelpSearch()
99141
if (watch) {
100142
const { context } = await import('esbuild')
143+
const rendererCtx = await context(renderer)
144+
const shellPreloadCtx = await context({
145+
...common,
146+
entryPoints: ['src/preload/shell.ts'],
147+
outfile: 'dist/shell-preload.cjs',
148+
})
101149
const mainCtx = await context({
102150
...common,
103151
entryPoints: ['src/main/index.ts'],
@@ -115,10 +163,18 @@ async function run(): Promise<void> {
115163
entryPoints: ['src/preload/browser/index.ts'],
116164
outfile: 'dist/browser-preload.cjs',
117165
})
118-
await Promise.all([mainCtx.watch(), preloadCtx.watch(), browserPreloadCtx.watch()])
166+
await Promise.all([
167+
mainCtx.watch(),
168+
preloadCtx.watch(),
169+
browserPreloadCtx.watch(),
170+
rendererCtx.watch(),
171+
shellPreloadCtx.watch(),
172+
])
119173
return
120174
}
121175
await Promise.all([
176+
build(renderer),
177+
build({ ...common, entryPoints: ['src/preload/shell.ts'], outfile: 'dist/shell-preload.cjs' }),
122178
build({ ...common, entryPoints: ['src/main/index.ts'], outfile: 'dist/main.cjs' }),
123179
build({ ...common, entryPoints: ['src/preload/index.ts'], outfile: 'dist/preload.cjs' }),
124180
build({

0 commit comments

Comments
 (0)