Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
d48d985
fix(knowledge): remove connections with durable background cleanup (#…
icecrasher321 Sep 15, 2026
bb20239
fix(search): preserve filters and report incomplete results (#7855)
waleedlatif1 Sep 15, 2026
8fea7cf
feat(cleanup): add per-type row limits to existing jobs (#7842)
TheodoreSpeaks Sep 15, 2026
d61006a
fix(desktop): share browser modal and status page presentation (#7856)
waleedlatif1 Sep 15, 2026
ae4981e
fix(workflows): preserve file inputs in workflow testing (#7857)
icecrasher321 Sep 15, 2026
3856d9e
fix(integ): bound credential scans and classify disabled TTL (#7860)
TheodoreSpeaks Sep 15, 2026
585e8f7
fix(chat): download generated files without stale storage URLs (#7858)
icecrasher321 Sep 15, 2026
f21bf92
fix(agent): authorize workspace attachments through execution delegat…
icecrasher321 Sep 15, 2026
22c149a
fix(settings): keep organization settings inline without Sim Search (…
TheodoreSpeaks Sep 15, 2026
aeec08a
fix(activity): simplify outcome summaries and show three actions (#7866)
waleedlatif1 Sep 15, 2026
5bca037
feat(sso): open Sim from an identity provider's app dashboard (#7865)
waleedlatif1 Sep 15, 2026
1e7c3dc
fix(cleanup): preserve log rows when file deletion fails (#7869)
TheodoreSpeaks Sep 15, 2026
8e14e4d
fix(provenance): align file matching and report withheld attachments …
icecrasher321 Sep 16, 2026
21291d7
fix(files): hydrate chat attachments and reject blank download URLs (…
icecrasher321 Sep 16, 2026
f4845cc
fix(tables): rework lock settings as Table Security and gate locked a…
j15z Sep 16, 2026
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
3 changes: 2 additions & 1 deletion .github/workflows/test-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,9 @@ jobs:

- name: Verify OAuth lifecycle and SCIM membership guards in PostgreSQL
working-directory: apps/sim
# These suites share a schema and install triggers; parallel files can deadlock DDL against cleanup.
run: >-
bunx vitest run
bunx vitest run --no-file-parallelism
lib/auth/oauth-token-family.postgres.test.ts
lib/auth/oauth-provider-lifecycle.postgres.test.ts
app/api/auth/oauth2/token/route.postgres.test.ts
Expand Down
60 changes: 53 additions & 7 deletions apps/desktop/e2e/smoke.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,55 @@ test.describe('desktop shell smoke', () => {
await expect(window.locator('#detail')).toHaveAttribute('role', 'status')
})

test('offline title-bar geometry follows native fullscreen state across reloads', async () => {
test.skip(process.platform !== 'darwin', 'The traffic-light lane is macOS-specific')
app = await launchApp('http://127.0.0.1:1')
const window = await app.firstWindow()
await expect(window.locator('#server')).toBeVisible()
await expect(window.locator('html')).toHaveAttribute('data-sim-desktop-title-bar', 'inset')
await app.evaluate(({ BrowserWindow }) => {
BrowserWindow.getAllWindows()[0].setFullScreen(true)
})
await expect(window.locator('html')).toHaveAttribute('data-sim-desktop-title-bar', 'fullscreen')
await window.reload()
await expect(window.locator('#server')).toBeVisible()
await expect(window.locator('html')).toHaveAttribute('data-sim-desktop-title-bar', 'fullscreen')
await expect(window.locator('.desktop-title-bar-page')).toHaveCSS('padding-top', '0px')
await app.evaluate(({ BrowserWindow }) => {
BrowserWindow.getAllWindows()[0].setFullScreen(false)
})
await expect(window.locator('html')).toHaveAttribute('data-sim-desktop-title-bar', 'inset')
})

test('bundled dialogs follow the app theme independently of the system theme', async () => {
app = await launchApp(origin)
const window = await app.firstWindow()
await expect(window.locator('#app')).toBeVisible()
await app.evaluate(({ nativeTheme }) => {
nativeTheme.themeSource = 'light'
})
await window.evaluate(() => {
document.documentElement.className = 'dark'
})
const dialogPromise = app.waitForEvent('window')
await app.evaluate(({ BrowserWindow }) => {
BrowserWindow.getAllWindows()[0].webContents.emit('unresponsive')
})
const prompt = await dialogPromise
await expect(prompt.getByRole('dialog')).toBeVisible()
await expect(prompt.locator('html')).toHaveClass('dark')
await expect(prompt.locator('html')).toHaveCSS('color-scheme', 'dark')
await expect(prompt.locator('#dialog-message')).toHaveCSS('-webkit-font-smoothing', 'auto')
await expect(prompt.locator('#dialog-message')).toHaveCSS('font-weight', '400')
await expect(prompt.locator('#dialog-message')).toHaveCSS('font-size', '14px')
await window.evaluate(() => {
document.documentElement.className = 'light'
})
await expect(prompt.locator('html')).toHaveClass('light')
await expect(prompt.locator('html')).toHaveCSS('color-scheme', 'light')
await prompt.getByRole('button', { name: 'Wait', exact: true }).click()
})

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()
Expand All @@ -188,7 +237,9 @@ test.describe('desktop shell smoke', () => {
BrowserWindow.getAllWindows()[0].webContents.emit('unresponsive')
})
const prompt = await dialogPromise
await expect(prompt.getByRole('dialog', { name: 'Sim', exact: true })).toBeVisible()
await expect(
prompt.getByRole('dialog', { name: 'Sim isn’t responding', exact: true })
).toBeVisible()
await expect(prompt.getByText('Sim isn’t responding')).toBeVisible()
await expect(prompt.getByRole('button', { name: 'Wait', exact: true })).toBeFocused()
await expect
Expand All @@ -213,10 +264,7 @@ test.describe('desktop shell smoke', () => {
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'
),
text: Array.from({ length: 80 }, (_, index) => `Diagnostic detail ${index + 1}`).join('\n'),
type: 'warning',
buttons: ['Wait', 'Reload'],
defaultId: 0,
Expand Down Expand Up @@ -291,8 +339,6 @@ test.describe('desktop shell smoke', () => {
})

const closed = picker.waitForEvent('close')
// The main process destroys the window on the key-down, so the key-up half
// of `press` has no target to reach; the close event is the assertion.
await picker.keyboard.press('Escape').catch(() => {})
await closed
expect(app.windows()).toHaveLength(1)
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/electron-builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ files:
- dist/**
- static/**
- package.json
- from: ../sim/public/brand/fonts
- from: ../sim/app/_styles/fonts/season
to: static
filter:
- SeasonSansUprightsVF.woff2
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/dialogs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ describe('showShellDialog', () => {
resize?.(sender(win), Number.NaN)
expect(win.setContentSize).not.toHaveBeenCalled()
resize?.(sender(win), 100000)
expect(win.setContentSize).toHaveBeenCalledWith(500, 820)
expect(win.setContentSize).toHaveBeenCalledWith(440, 820)
respond(win, 0)
await result
})
Expand Down
11 changes: 6 additions & 5 deletions apps/desktop/src/main/dialogs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import { getErrorMessage } from '@sim/utils/errors'
import type { MessageBoxOptions, MessageBoxReturnValue } from 'electron'
import { app, BrowserWindow, dialog, nativeTheme, session } from 'electron'
import { attachLocalPageProtocol, localPageUrl } from '@/main/local-pages'
import { attachShellTheme, backgroundColorFor, getShellTheme } from '@/main/shell-theme'
import { attachShellWindowSizing, isShellWindowSender } from '@/main/shell-window'
import { createSecureWebPreferences } from '@/main/window-preferences'
import type { ShellDialogConfiguration } from '@/shared/shell'

const logger = createLogger('DesktopDialogs')
const DIALOG_WIDTH = 500
const DIALOG_WIDTH = 440
const DIALOG_PARTITION = 'shell-dialogs'

interface ShellDialogOptions extends MessageBoxOptions {
Expand Down Expand Up @@ -39,9 +40,8 @@ export function showShellDialog(
buttons.findIndex((label) => /^(cancel|no|close|ok)$/i.test(label))
)
const configuration: ShellDialogConfiguration = {
title: options.title ?? 'Sim',
message: options.message,
detail: options.detail ?? '',
title: options.title ?? options.message,
text: [options.title ? options.message : '', options.detail].filter(Boolean).join('\n\n'),
buttons,
defaultId: options.defaultId ?? 0,
cancelId,
Expand All @@ -66,7 +66,7 @@ export function showShellDialog(
fullscreenable: false,
show: false,
title: configuration.title,
backgroundColor: nativeTheme.shouldUseDarkColors ? '#1b1b1b' : '#ffffff',
backgroundColor: backgroundColorFor(getShellTheme(), nativeTheme.shouldUseDarkColors),
...(parent && !parent.isDestroyed() ? { parent, modal: true } : {}),
webPreferences: createSecureWebPreferences(
DIALOG_PARTITION,
Expand All @@ -75,6 +75,7 @@ export function showShellDialog(
),
})
const pageUrl = localPageUrl('dialog.html')
attachShellTheme(win)
let settled = false
const finish = (response: number) => {
if (settled) return
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ import {
readSessionUserId,
resolveStartRoute,
} from '@/main/session-lifecycle'
import { setShellTheme } from '@/main/shell-theme'
import { attachTelemetryPolicy } from '@/main/telemetry-policy'
import { TerminalRegistry } from '@/main/terminal/registry'
import { installTray, type TrayHandle } from '@/main/tray'
Expand Down Expand Up @@ -110,6 +111,7 @@ function main(): void {

const userDataPath = app.getPath('userData')
const config = createConfigStore(join(userDataPath, 'settings.json'))
setShellTheme(config.get('themeBackground'))
initializeAccountDataRecovery(join(userDataPath, 'account-data-teardown-required.json'))
const recoveryOrigin = getAccountDataTeardownOrigin()
if (isAccountDataTeardownRequired() && recoveryOrigin && !config.isPersistenceAvailable()) {
Expand Down
4 changes: 3 additions & 1 deletion apps/desktop/src/main/ipc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -665,13 +665,15 @@ describe('registerIpcHandlers', () => {
expect(deps.settings.chooseBrowserDownloadDirectory).toHaveBeenCalledTimes(1)
})

it('reports native fullscreen state only to the app origin', async () => {
it('reports native fullscreen state only to the app origin and bundled pages', async () => {
const { invoke } = collectHandlers()
const getWindowState = invoke.get('desktop:window-state:get')

expect(await getWindowState?.(evilEvent)).toEqual({ isFullScreen: false })
expect(await getWindowState?.(appEvent)).toEqual({ isFullScreen: true })
expect(deps.getWindowState).toHaveBeenCalledWith(appSender)
expect(await getWindowState?.(localPageEvent)).toEqual({ isFullScreen: true })
expect(deps.getWindowState).toHaveBeenCalledWith(localPageSender)
})

it('restricts shell-control channels to bundled local pages', () => {
Expand Down
12 changes: 10 additions & 2 deletions apps/desktop/src/main/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,14 +376,15 @@ export interface IpcDeps {
* - `app-origin`: only the remote app origin (main window pages).
* - `local-page`: only the bundled pages served from the shell's own scheme
* (offline, server) — shell control.
* - `app-or-local-page`: read-only window state used by both hosted and bundled pages.
* - `browser-page`: only the built-in browser's own tabs, identified by
* WebContents rather than by URL. These carry reports from the browser
* preload about untrusted pages, so they are the one inbound surface whose
* sender is not the app — the payload is treated as a claim to verify, never
* as an instruction.
* - `any`: sender-independent channels that validate their input instead.
*/
type ChannelGate = 'app-origin' | 'local-page' | 'browser-page' | 'any'
type ChannelGate = 'app-origin' | 'local-page' | 'app-or-local-page' | 'browser-page' | 'any'

/**
* A desktop surface the user can switch off. Channels that drive one are
Expand Down Expand Up @@ -816,7 +817,9 @@ export function registerIpcHandlers(deps: IpcDeps): void {
},
'desktop:window-state:get': {
kind: 'invoke',
gate: 'app-origin',
gate: 'app-or-local-page',
deviationReason:
'Bundled offline pages share the app title-bar geometry and need their own native fullscreen state.',
passSender: true,
denied: { isFullScreen: false },
handler: (sender) => deps.getWindowState(sender as WebContents),
Expand Down Expand Up @@ -1857,6 +1860,11 @@ export function registerIpcHandlers(deps: IpcDeps): void {
const senderAllowed = (event: IpcMainEvent | IpcMainInvokeEvent, gate: ChannelGate): boolean => {
if (gate === 'any') return true
if (gate === 'app-origin') return isAppOriginSender(event, deps.appOrigin())
if (gate === 'app-or-local-page') {
return (
isAppOriginSender(event, deps.appOrigin()) || isLocalPageSender(event, deps.isLocalPageUrl)
)
}
if (gate === 'browser-page') return isAgentWebContents(event.sender)
return isLocalPageSender(event, deps.isLocalPageUrl)
}
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/main/local-pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,13 +167,13 @@ async function readFirst(rootDirs: readonly string[], name: string): Promise<Buf
* build: renderer bundles live in `dist/renderer`, and page shells live in
* `static/`, inside the packaged asar as well as in an unpackaged checkout. The brand font is copied into `static/` only when
* packaging (electron-builder.yml); an unpackaged run reads it from the web
* app's public fonts instead, so nothing generated has to exist in the tree
* app's Next font source instead, so nothing generated has to exist in the tree
* and a cached build restores everything the pages need.
*/
function localPageRoots(): string[] {
const roots = [join(__dirname, 'renderer'), join(__dirname, '..', 'static')]
if (!app.isPackaged) {
roots.push(join(__dirname, '..', '..', 'sim', 'public', 'brand', 'fonts'))
roots.push(join(__dirname, '..', '..', 'sim', 'app', '_styles', 'fonts', 'season'))
}
return roots
}
Expand Down
19 changes: 5 additions & 14 deletions apps/desktop/src/main/server-window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import type { ConfigStore, DesktopSettings } from '@/main/config'
import { canonicalOrigin, isSimCloudOrigin, validateOriginInput } from '@/main/config'
import { showShellDialog } from '@/main/dialogs'
import { attachLocalPageProtocol, localPageUrl } from '@/main/local-pages'
import { attachShellTheme, backgroundColorFor, getShellTheme } from '@/main/shell-theme'
import { attachShellWindowSizing } from '@/main/shell-window'
import { backgroundColorFor, setupPermissionHandlers } from '@/main/window'
import { setupPermissionHandlers } from '@/main/window'
import { createSecureWebPreferences } from '@/main/window-preferences'

const logger = createLogger('DesktopServerWindow')
Expand Down Expand Up @@ -103,13 +104,7 @@ export interface ServerWindowHandle {
*/
export function createServerWindow(deps: ServerWindowDeps): ServerWindowHandle {
let win: BrowserWindow | null = null
/**
* Serializes the destructive part of a change, the way the sign-out
* coordinator guards its own teardown. The picker re-enables its button
* while a request is pending, and the IPC boundary is reachable regardless
* of what the page does, so without this two changes could interleave their
* teardown and their write and let the later write pick the next server.
*/
/** Prevents concurrent IPC requests from interleaving server teardown and persistence. */
let changeInFlight = false

const getConfiguration = (): DesktopServerConfiguration => {
Expand Down Expand Up @@ -149,12 +144,7 @@ export function createServerWindow(deps: ServerWindowDeps): ServerWindowHandle {
title: 'Sim Server',
frame: false,
show: false,
// System preference only, unlike the main window: that one pre-paints for
// the web app it is about to load, whose theme the user picked in Sim.
// This window loads a bundled page that follows `prefers-color-scheme`,
// so honouring the stored web-app theme here would pre-paint dark behind
// a page about to render light whenever the two disagree.
backgroundColor: backgroundColorFor(undefined, nativeTheme.shouldUseDarkColors),
backgroundColor: backgroundColorFor(getShellTheme(), nativeTheme.shouldUseDarkColors),
// Modal only when there is a live parent to attach to. A shell whose
// window is gone (or never opened, because the origin failed to load)
// still has to be able to reach this.
Expand All @@ -169,6 +159,7 @@ export function createServerWindow(deps: ServerWindowDeps): ServerWindowHandle {
// ways out must therefore work without the page: Escape is handled here,
// and a page that fails to load closes the window instead of leaving a
// blank sheet nothing can dismiss.
attachShellTheme(win)
const opened = win
let closed = false
const closeOpened = () => {
Expand Down
52 changes: 52 additions & 0 deletions apps/desktop/src/main/shell-theme.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'

vi.mock('electron', () => import('@/test/electron-mock'))

import type { BrowserWindow as ElectronWindow } from 'electron'
import { attachShellTheme, getShellTheme, setShellTheme } from '@/main/shell-theme'
import { BrowserWindow } from '@/test/electron-mock'

beforeEach(() => {
setShellTheme(undefined)
vi.clearAllMocks()
})

describe('shell theme', () => {
it('retains the resolved app theme and updates only bundled pages', () => {
const local = new BrowserWindow({})
const app = new BrowserWindow({})
local.webContents.getURL.mockReturnValue('sim-shell://pages/dialog.html')
app.webContents.getURL.mockReturnValue('https://sim.example')
attachShellTheme(local as unknown as ElectronWindow)
attachShellTheme(app as unknown as ElectronWindow)
setShellTheme('dark')
expect(getShellTheme()).toBe('dark')
expect(local.webContents.send).toHaveBeenCalledWith('shell:theme-changed', 'dark')
expect(app.webContents.send).not.toHaveBeenCalled()
setShellTheme('dark')
expect(local.webContents.send).toHaveBeenCalledOnce()
local.on.mock.calls.find(([event]) => event === 'closed')?.[1]()
app.on.mock.calls.find(([event]) => event === 'closed')?.[1]()
setShellTheme('light')
expect(local.webContents.send).toHaveBeenCalledOnce()
})

it('rejects theme reads from foreign documents and subframes', () => {
const win = new BrowserWindow({})
attachShellTheme(win as unknown as ElectronWindow)
const read = win.webContents.ipc.handle.mock.calls.find(
([channel]) => channel === 'shell:get-theme'
)?.[1]
if (!read) throw new Error('Missing theme handler')
const event = { sender: win.webContents, senderFrame: win.webContents.mainFrame }
win.webContents.mainFrame.url = 'sim-shell://pages/offline.html?kind=dns'
setShellTheme('light')
expect(read(event)).toBe('light')
expect(() => read({ ...event, senderFrame: { url: event.senderFrame.url } })).toThrow(
'Untrusted'
)
win.webContents.mainFrame.url = 'https://untrusted.example'
expect(() => read(event)).toThrow('Untrusted')
win.on.mock.calls.find(([event]) => event === 'closed')?.[1]()
})
})
Loading
Loading