Skip to content

Commit c0f4717

Browse files
authored
v0.8.39: search and file handling fixes, organization settings, bounded cleanup
2 parents d7ef91a + f4845cc commit c0f4717

207 files changed

Lines changed: 8111 additions & 1923 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/test-build.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,9 @@ jobs:
8787

8888
- name: Verify OAuth lifecycle and SCIM membership guards in PostgreSQL
8989
working-directory: apps/sim
90+
# These suites share a schema and install triggers; parallel files can deadlock DDL against cleanup.
9091
run: >-
91-
bunx vitest run
92+
bunx vitest run --no-file-parallelism
9293
lib/auth/oauth-token-family.postgres.test.ts
9394
lib/auth/oauth-provider-lifecycle.postgres.test.ts
9495
app/api/auth/oauth2/token/route.postgres.test.ts

apps/desktop/e2e/smoke.spec.ts

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,55 @@ test.describe('desktop shell smoke', () => {
179179
await expect(window.locator('#detail')).toHaveAttribute('role', 'status')
180180
})
181181

182+
test('offline title-bar geometry follows native fullscreen state across reloads', async () => {
183+
test.skip(process.platform !== 'darwin', 'The traffic-light lane is macOS-specific')
184+
app = await launchApp('http://127.0.0.1:1')
185+
const window = await app.firstWindow()
186+
await expect(window.locator('#server')).toBeVisible()
187+
await expect(window.locator('html')).toHaveAttribute('data-sim-desktop-title-bar', 'inset')
188+
await app.evaluate(({ BrowserWindow }) => {
189+
BrowserWindow.getAllWindows()[0].setFullScreen(true)
190+
})
191+
await expect(window.locator('html')).toHaveAttribute('data-sim-desktop-title-bar', 'fullscreen')
192+
await window.reload()
193+
await expect(window.locator('#server')).toBeVisible()
194+
await expect(window.locator('html')).toHaveAttribute('data-sim-desktop-title-bar', 'fullscreen')
195+
await expect(window.locator('.desktop-title-bar-page')).toHaveCSS('padding-top', '0px')
196+
await app.evaluate(({ BrowserWindow }) => {
197+
BrowserWindow.getAllWindows()[0].setFullScreen(false)
198+
})
199+
await expect(window.locator('html')).toHaveAttribute('data-sim-desktop-title-bar', 'inset')
200+
})
201+
202+
test('bundled dialogs follow the app theme independently of the system theme', async () => {
203+
app = await launchApp(origin)
204+
const window = await app.firstWindow()
205+
await expect(window.locator('#app')).toBeVisible()
206+
await app.evaluate(({ nativeTheme }) => {
207+
nativeTheme.themeSource = 'light'
208+
})
209+
await window.evaluate(() => {
210+
document.documentElement.className = 'dark'
211+
})
212+
const dialogPromise = app.waitForEvent('window')
213+
await app.evaluate(({ BrowserWindow }) => {
214+
BrowserWindow.getAllWindows()[0].webContents.emit('unresponsive')
215+
})
216+
const prompt = await dialogPromise
217+
await expect(prompt.getByRole('dialog')).toBeVisible()
218+
await expect(prompt.locator('html')).toHaveClass('dark')
219+
await expect(prompt.locator('html')).toHaveCSS('color-scheme', 'dark')
220+
await expect(prompt.locator('#dialog-message')).toHaveCSS('-webkit-font-smoothing', 'auto')
221+
await expect(prompt.locator('#dialog-message')).toHaveCSS('font-weight', '400')
222+
await expect(prompt.locator('#dialog-message')).toHaveCSS('font-size', '14px')
223+
await window.evaluate(() => {
224+
document.documentElement.className = 'light'
225+
})
226+
await expect(prompt.locator('html')).toHaveClass('light')
227+
await expect(prompt.locator('html')).toHaveCSS('color-scheme', 'light')
228+
await prompt.getByRole('button', { name: 'Wait', exact: true }).click()
229+
})
230+
182231
test('recovery messages use an isolated EMCN dialog with a safe keyboard default', async () => {
183232
app = await launchApp('http://127.0.0.1:1')
184233
const window = await app.firstWindow()
@@ -188,7 +237,9 @@ test.describe('desktop shell smoke', () => {
188237
BrowserWindow.getAllWindows()[0].webContents.emit('unresponsive')
189238
})
190239
const prompt = await dialogPromise
191-
await expect(prompt.getByRole('dialog', { name: 'Sim', exact: true })).toBeVisible()
240+
await expect(
241+
prompt.getByRole('dialog', { name: 'Sim isn’t responding', exact: true })
242+
).toBeVisible()
192243
await expect(prompt.getByText('Sim isn’t responding')).toBeVisible()
193244
await expect(prompt.getByRole('button', { name: 'Wait', exact: true })).toBeFocused()
194245
await expect
@@ -213,10 +264,7 @@ test.describe('desktop shell smoke', () => {
213264
win.webContents.ipc.removeHandler('shell:configuration')
214265
win.webContents.ipc.handle('shell:configuration', () => ({
215266
title: 'Long recovery message',
216-
message: 'Recovery details',
217-
detail: Array.from({ length: 80 }, (_, index) => `Diagnostic detail ${index + 1}`).join(
218-
'\n'
219-
),
267+
text: Array.from({ length: 80 }, (_, index) => `Diagnostic detail ${index + 1}`).join('\n'),
220268
type: 'warning',
221269
buttons: ['Wait', 'Reload'],
222270
defaultId: 0,
@@ -291,8 +339,6 @@ test.describe('desktop shell smoke', () => {
291339
})
292340

293341
const closed = picker.waitForEvent('close')
294-
// The main process destroys the window on the key-down, so the key-up half
295-
// of `press` has no target to reach; the close event is the assertion.
296342
await picker.keyboard.press('Escape').catch(() => {})
297343
await closed
298344
expect(app.windows()).toHaveLength(1)

apps/desktop/electron-builder.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ files:
1010
- dist/**
1111
- static/**
1212
- package.json
13-
- from: ../sim/public/brand/fonts
13+
- from: ../sim/app/_styles/fonts/season
1414
to: static
1515
filter:
1616
- SeasonSansUprightsVF.woff2

apps/desktop/src/main/dialogs.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ describe('showShellDialog', () => {
121121
resize?.(sender(win), Number.NaN)
122122
expect(win.setContentSize).not.toHaveBeenCalled()
123123
resize?.(sender(win), 100000)
124-
expect(win.setContentSize).toHaveBeenCalledWith(500, 820)
124+
expect(win.setContentSize).toHaveBeenCalledWith(440, 820)
125125
respond(win, 0)
126126
await result
127127
})

apps/desktop/src/main/dialogs.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ import { getErrorMessage } from '@sim/utils/errors'
44
import type { MessageBoxOptions, MessageBoxReturnValue } from 'electron'
55
import { app, BrowserWindow, dialog, nativeTheme, session } from 'electron'
66
import { attachLocalPageProtocol, localPageUrl } from '@/main/local-pages'
7+
import { attachShellTheme, backgroundColorFor, getShellTheme } from '@/main/shell-theme'
78
import { attachShellWindowSizing, isShellWindowSender } from '@/main/shell-window'
89
import { createSecureWebPreferences } from '@/main/window-preferences'
910
import type { ShellDialogConfiguration } from '@/shared/shell'
1011

1112
const logger = createLogger('DesktopDialogs')
12-
const DIALOG_WIDTH = 500
13+
const DIALOG_WIDTH = 440
1314
const DIALOG_PARTITION = 'shell-dialogs'
1415

1516
interface ShellDialogOptions extends MessageBoxOptions {
@@ -39,9 +40,8 @@ export function showShellDialog(
3940
buttons.findIndex((label) => /^(cancel|no|close|ok)$/i.test(label))
4041
)
4142
const configuration: ShellDialogConfiguration = {
42-
title: options.title ?? 'Sim',
43-
message: options.message,
44-
detail: options.detail ?? '',
43+
title: options.title ?? options.message,
44+
text: [options.title ? options.message : '', options.detail].filter(Boolean).join('\n\n'),
4545
buttons,
4646
defaultId: options.defaultId ?? 0,
4747
cancelId,
@@ -66,7 +66,7 @@ export function showShellDialog(
6666
fullscreenable: false,
6767
show: false,
6868
title: configuration.title,
69-
backgroundColor: nativeTheme.shouldUseDarkColors ? '#1b1b1b' : '#ffffff',
69+
backgroundColor: backgroundColorFor(getShellTheme(), nativeTheme.shouldUseDarkColors),
7070
...(parent && !parent.isDestroyed() ? { parent, modal: true } : {}),
7171
webPreferences: createSecureWebPreferences(
7272
DIALOG_PARTITION,
@@ -75,6 +75,7 @@ export function showShellDialog(
7575
),
7676
})
7777
const pageUrl = localPageUrl('dialog.html')
78+
attachShellTheme(win)
7879
let settled = false
7980
const finish = (response: number) => {
8081
if (settled) return

apps/desktop/src/main/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ import {
7878
readSessionUserId,
7979
resolveStartRoute,
8080
} from '@/main/session-lifecycle'
81+
import { setShellTheme } from '@/main/shell-theme'
8182
import { attachTelemetryPolicy } from '@/main/telemetry-policy'
8283
import { TerminalRegistry } from '@/main/terminal/registry'
8384
import { installTray, type TrayHandle } from '@/main/tray'
@@ -110,6 +111,7 @@ function main(): void {
110111

111112
const userDataPath = app.getPath('userData')
112113
const config = createConfigStore(join(userDataPath, 'settings.json'))
114+
setShellTheme(config.get('themeBackground'))
113115
initializeAccountDataRecovery(join(userDataPath, 'account-data-teardown-required.json'))
114116
const recoveryOrigin = getAccountDataTeardownOrigin()
115117
if (isAccountDataTeardownRequired() && recoveryOrigin && !config.isPersistenceAvailable()) {

apps/desktop/src/main/ipc.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -665,13 +665,15 @@ describe('registerIpcHandlers', () => {
665665
expect(deps.settings.chooseBrowserDownloadDirectory).toHaveBeenCalledTimes(1)
666666
})
667667

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

672672
expect(await getWindowState?.(evilEvent)).toEqual({ isFullScreen: false })
673673
expect(await getWindowState?.(appEvent)).toEqual({ isFullScreen: true })
674674
expect(deps.getWindowState).toHaveBeenCalledWith(appSender)
675+
expect(await getWindowState?.(localPageEvent)).toEqual({ isFullScreen: true })
676+
expect(deps.getWindowState).toHaveBeenCalledWith(localPageSender)
675677
})
676678

677679
it('restricts shell-control channels to bundled local pages', () => {

apps/desktop/src/main/ipc.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -376,14 +376,15 @@ export interface IpcDeps {
376376
* - `app-origin`: only the remote app origin (main window pages).
377377
* - `local-page`: only the bundled pages served from the shell's own scheme
378378
* (offline, server) — shell control.
379+
* - `app-or-local-page`: read-only window state used by both hosted and bundled pages.
379380
* - `browser-page`: only the built-in browser's own tabs, identified by
380381
* WebContents rather than by URL. These carry reports from the browser
381382
* preload about untrusted pages, so they are the one inbound surface whose
382383
* sender is not the app — the payload is treated as a claim to verify, never
383384
* as an instruction.
384385
* - `any`: sender-independent channels that validate their input instead.
385386
*/
386-
type ChannelGate = 'app-origin' | 'local-page' | 'browser-page' | 'any'
387+
type ChannelGate = 'app-origin' | 'local-page' | 'app-or-local-page' | 'browser-page' | 'any'
387388

388389
/**
389390
* A desktop surface the user can switch off. Channels that drive one are
@@ -816,7 +817,9 @@ export function registerIpcHandlers(deps: IpcDeps): void {
816817
},
817818
'desktop:window-state:get': {
818819
kind: 'invoke',
819-
gate: 'app-origin',
820+
gate: 'app-or-local-page',
821+
deviationReason:
822+
'Bundled offline pages share the app title-bar geometry and need their own native fullscreen state.',
820823
passSender: true,
821824
denied: { isFullScreen: false },
822825
handler: (sender) => deps.getWindowState(sender as WebContents),
@@ -1857,6 +1860,11 @@ export function registerIpcHandlers(deps: IpcDeps): void {
18571860
const senderAllowed = (event: IpcMainEvent | IpcMainInvokeEvent, gate: ChannelGate): boolean => {
18581861
if (gate === 'any') return true
18591862
if (gate === 'app-origin') return isAppOriginSender(event, deps.appOrigin())
1863+
if (gate === 'app-or-local-page') {
1864+
return (
1865+
isAppOriginSender(event, deps.appOrigin()) || isLocalPageSender(event, deps.isLocalPageUrl)
1866+
)
1867+
}
18601868
if (gate === 'browser-page') return isAgentWebContents(event.sender)
18611869
return isLocalPageSender(event, deps.isLocalPageUrl)
18621870
}

apps/desktop/src/main/local-pages.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,13 +167,13 @@ async function readFirst(rootDirs: readonly string[], name: string): Promise<Buf
167167
* build: renderer bundles live in `dist/renderer`, and page shells live in
168168
* `static/`, inside the packaged asar as well as in an unpackaged checkout. The brand font is copied into `static/` only when
169169
* packaging (electron-builder.yml); an unpackaged run reads it from the web
170-
* app's public fonts instead, so nothing generated has to exist in the tree
170+
* app's Next font source instead, so nothing generated has to exist in the tree
171171
* and a cached build restores everything the pages need.
172172
*/
173173
function localPageRoots(): string[] {
174174
const roots = [join(__dirname, 'renderer'), join(__dirname, '..', 'static')]
175175
if (!app.isPackaged) {
176-
roots.push(join(__dirname, '..', '..', 'sim', 'public', 'brand', 'fonts'))
176+
roots.push(join(__dirname, '..', '..', 'sim', 'app', '_styles', 'fonts', 'season'))
177177
}
178178
return roots
179179
}

apps/desktop/src/main/server-window.ts

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import type { ConfigStore, DesktopSettings } from '@/main/config'
77
import { canonicalOrigin, isSimCloudOrigin, validateOriginInput } from '@/main/config'
88
import { showShellDialog } from '@/main/dialogs'
99
import { attachLocalPageProtocol, localPageUrl } from '@/main/local-pages'
10+
import { attachShellTheme, backgroundColorFor, getShellTheme } from '@/main/shell-theme'
1011
import { attachShellWindowSizing } from '@/main/shell-window'
11-
import { backgroundColorFor, setupPermissionHandlers } from '@/main/window'
12+
import { setupPermissionHandlers } from '@/main/window'
1213
import { createSecureWebPreferences } from '@/main/window-preferences'
1314

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

115110
const getConfiguration = (): DesktopServerConfiguration => {
@@ -149,12 +144,7 @@ export function createServerWindow(deps: ServerWindowDeps): ServerWindowHandle {
149144
title: 'Sim Server',
150145
frame: false,
151146
show: false,
152-
// System preference only, unlike the main window: that one pre-paints for
153-
// the web app it is about to load, whose theme the user picked in Sim.
154-
// This window loads a bundled page that follows `prefers-color-scheme`,
155-
// so honouring the stored web-app theme here would pre-paint dark behind
156-
// a page about to render light whenever the two disagree.
157-
backgroundColor: backgroundColorFor(undefined, nativeTheme.shouldUseDarkColors),
147+
backgroundColor: backgroundColorFor(getShellTheme(), nativeTheme.shouldUseDarkColors),
158148
// Modal only when there is a live parent to attach to. A shell whose
159149
// window is gone (or never opened, because the origin failed to load)
160150
// still has to be able to reach this.
@@ -169,6 +159,7 @@ export function createServerWindow(deps: ServerWindowDeps): ServerWindowHandle {
169159
// ways out must therefore work without the page: Escape is handled here,
170160
// and a page that fails to load closes the window instead of leaving a
171161
// blank sheet nothing can dismiss.
162+
attachShellTheme(win)
172163
const opened = win
173164
let closed = false
174165
const closeOpened = () => {

0 commit comments

Comments
 (0)