Skip to content

Commit adc557a

Browse files
merge: staging into feat/table-views
table_views migration renumbered 0274 -> 0275; staging shipped its own 0274 (file_folder_cutover_reconcile). Regenerated via drizzle-kit, chain verified.
2 parents c6acc62 + e369997 commit adc557a

122 files changed

Lines changed: 24093 additions & 845 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/ci.yml

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -710,8 +710,19 @@ jobs:
710710
# and are always superseded by the next stable. The run-attempt
711711
# suffix keeps re-runs of the same workflow from colliding on the
712712
# tag while preserving semver ordering.
713-
LATEST="$(gh release list --exclude-pre-releases --limit 1 --json tagName --jq '.[0].tagName' || true)"
714-
LATEST="${LATEST:-v0.0.0}"
713+
# Fail loudly if the query itself fails: silently falling back to
714+
# v0.0.0 would publish a channel build that sorts below the shipped
715+
# stable, and installed shells would never see it as an update.
716+
if ! LATEST="$(gh release list --exclude-pre-releases --limit 1 --json tagName --jq '.[0].tagName')"; then
717+
echo "::error::Could not query the latest stable release."
718+
exit 1
719+
fi
720+
# An empty release list makes jq print "null", which ${VAR:-default}
721+
# does not treat as empty. Anything that is not a bare vX.Y.Z means
722+
# "no stable release to build on top of" — start the channel at 0.0.1.
723+
if [[ ! "$LATEST" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
724+
LATEST="v0.0.0"
725+
fi
715726
IFS='.' read -r MAJOR MINOR PATCH <<< "${LATEST#v}"
716727
TAG="v${MAJOR}.${MINOR}.$((PATCH + 1))-${CHANNEL}.${GITHUB_RUN_NUMBER}.${GITHUB_RUN_ATTEMPT}"
717728
NOTES="Automated ${CHANNEL}-channel desktop build from ${GITHUB_REF_NAME} @ ${GITHUB_SHA::7}."

apps/desktop/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Sim Desktop (macOS)
22

3-
A thin Electron shell around the hosted Sim web app. The renderer loads the configured origin (default `https://sim.ai`) as a normal top-level page in a bundled, pinned Chromium — rendering is identical to Chrome of that version on every machine. No UI is re-implemented and no server stack is bundled.
3+
A thin Electron shell around the hosted Sim web app. The renderer loads the configured origin (default `https://www.sim.ai` — the origin the server actually serves; the apex 301s there) as a normal top-level page in a bundled, pinned Chromium — rendering is identical to Chrome of that version on every machine. No UI is re-implemented and no server stack is bundled.
44

55
## Layout
66

@@ -44,7 +44,7 @@ e2e/ # Playwright _electron smoke suite
4444
```bash
4545
bun install # workspace root
4646
cd apps/desktop
47-
bun run dev # bundle + launch against https://sim.ai
47+
bun run dev # bundle + launch against https://www.sim.ai
4848
SIM_DESKTOP_ORIGIN=http://localhost:3000 bun run dev # against local sim
4949
```
5050

@@ -74,7 +74,7 @@ Deviations from the original plan doc (deliberate):
7474

7575
## Provider matrix (U5 spike — keep current)
7676

77-
Host lists live in `src/main/navigation.ts` (`SYSTEM_BROWSER_IDP_HOSTS`, `IN_WINDOW_IDP_HOSTS`). Verified so far: Google + Microsoft blocked (by policy, not spike); GitHub assumed lenient. **Before GA, run the spike**: GitHub sign-in, consumer-Microsoft, a sample of integration connects (Notion, Slack, Linear, Atlassian, Box, Dropbox), SSO, and Turnstile-on-signup in a packaged build, then update the lists and this section.
77+
The host list lives in `src/main/navigation.ts` (`SYSTEM_BROWSER_IDP_HOSTS`) and now applies to **integration connects only** — sign-in always goes through the system-browser handoff whatever the IdP, so no provider needs to be classified as embed-tolerant for login. (The old `IN_WINDOW_IDP_HOSTS` list existed to keep GitHub sign-in in-window; embedding a login is a one-way door in a chrome-less shell and splits better-auth's OAuth state across two cookie jars, so it was removed.) Verified so far: Google + Microsoft block embedding by policy. **Before GA, run the spike** for connects: a sample of integration connects (Notion, Slack, Linear, Atlassian, Box, Dropbox) plus SSO and Turnstile-on-signup in a packaged build, then update the list and this section.
7878

7979
## Web-app coupling contract (audit on web-app changes)
8080

apps/desktop/scripts/build.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const watch = process.argv.includes('--watch')
77
// non-prod environment): SIM_DESKTOP_DEFAULT_ORIGIN=https://www.dev.sim.ai.
88
// Baked into the bundle so it applies to fresh installs with no settings —
99
// unlike the SIM_DESKTOP_ORIGIN env var, which only affects terminal-launched
10-
// processes. Official builds leave it unset (default https://sim.ai).
10+
// processes. Official builds leave it unset (default https://www.sim.ai).
1111
const bakedDefaultOrigin = process.env.SIM_DESKTOP_DEFAULT_ORIGIN ?? ''
1212
if (
1313
bakedDefaultOrigin &&

apps/desktop/scripts/install-local.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,11 @@ const CHANNEL_FLAGS: Record<string, ChannelIdentity> = {
4040
appId: 'ai.sim.desktop.staging',
4141
origin: 'https://www.staging.sim.ai',
4242
},
43-
// Bare sim.ai, matching official prod builds (config.ts) — www.sim.ai
44-
// would land in a different cookie partition than a real install.
45-
'--prod': { name: 'Sim', appId: 'ai.sim.desktop', origin: 'https://sim.ai' },
43+
// Origin left unset, exactly as official prod builds do, so this install
44+
// resolves the same DEFAULT_ORIGIN (config.ts) and therefore the same cookie
45+
// partition as a real one. Naming an origin here re-creates the drift that
46+
// pinned prod to the apex while the server serves www.
47+
'--prod': { name: 'Sim', appId: 'ai.sim.desktop' },
4648
}
4749

4850
const DEFAULT_IDENTITY: ChannelIdentity = { name: 'Sim', appId: 'ai.sim.desktop' }

apps/desktop/src/main/config.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ const logger = createLogger('DesktopConfig')
1515
* define replaces the env read at bundle time, so a packaged app never
1616
* consults the runtime environment for this. http is accepted only for
1717
* loopback origins (the localhost dev-server channel).
18+
*
19+
* This must be the origin the server actually *serves*, not a hostname that
20+
* redirects to it. Every environment canonicalizes the apex to `www` (the load
21+
* balancer 301s `sim.ai` → `www.sim.ai`), and the navigation guards compare
22+
* origins for exact equality — so an apex origin here silently leaves every
23+
* page off-origin, which reclassifies social login as an integration connect
24+
* and strands the sign-in in the browser with no way back.
1825
*/
1926
function isValidBakedOrigin(origin: string | undefined): origin is string {
2027
if (!origin) return false
@@ -29,7 +36,7 @@ function isValidBakedOrigin(origin: string | undefined): origin is string {
2936

3037
export const DEFAULT_ORIGIN = isValidBakedOrigin(process.env.SIM_DESKTOP_DEFAULT_ORIGIN)
3138
? process.env.SIM_DESKTOP_DEFAULT_ORIGIN
32-
: 'https://sim.ai'
39+
: 'https://www.sim.ai'
3340

3441
/**
3542
* The environment a build is keyed to, derived from its baked default origin.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ describe('buildMenuTemplate', () => {
7474
expect(submenu(template, 'View').map((item) => item.label ?? item.role ?? item.type)).toEqual([
7575
'Toggle Sidebar',
7676
'separator',
77+
'Back',
7778
'Reload',
7879
'separator',
7980
'Actual Size',

apps/desktop/src/main/menu.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,21 @@ export function buildMenuTemplate(deps: MenuDeps): MenuItemConstructorOptions[]
5555
click: deps.toggleSidebar,
5656
},
5757
{ type: 'separator' },
58+
/**
59+
* The shell has no browser chrome, so an in-window integration connect
60+
* that leaves the app origin (the IdP's consent page) is otherwise a
61+
* one-way door for anyone who decides not to finish it.
62+
*/
63+
{
64+
label: 'Back',
65+
accelerator: 'CmdOrCtrl+[',
66+
click: withWindow((win) => {
67+
const history = win.webContents.navigationHistory
68+
if (history.canGoBack()) {
69+
history.goBack()
70+
}
71+
}),
72+
},
5873
{
5974
label: 'Reload',
6075
accelerator: 'CmdOrCtrl+R',

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
openExternalSafe,
1414
} from '@/main/navigation'
1515

16-
const APP = 'https://sim.ai'
16+
const APP = 'https://www.sim.ai'
1717

1818
describe('classifyNavigation', () => {
1919
it('keeps same-origin navigation in-app', () => {
@@ -65,12 +65,24 @@ describe('classifyNavigation', () => {
6565
).toBe('idp-system-connect')
6666
})
6767

68-
it('keeps verified-lenient IdPs in-window from the login surface', () => {
68+
it('routes every sign-in to the system browser, including IdPs that tolerate embedding', () => {
69+
// GitHub used to be kept in-window. Embedded, it is a one-way door (no
70+
// browser chrome to back out of) and it splits better-auth's OAuth state
71+
// cookie across two user agents, which comes back `state_mismatch`.
6972
expect(
7073
classifyNavigation('https://github.com/login/oauth/authorize?client_id=x', {
7174
appOrigin: APP,
7275
currentUrl: `${APP}/login`,
7376
})
77+
).toBe('idp-system-login')
78+
})
79+
80+
it('keeps the same IdP in-window when it is an integration connect', () => {
81+
expect(
82+
classifyNavigation('https://github.com/login/oauth/authorize?client_id=x', {
83+
appOrigin: APP,
84+
currentUrl: `${APP}/workspace/ws1/integrations/github`,
85+
})
7486
).toBe('idp-in-window')
7587
})
7688

apps/desktop/src/main/navigation.ts

Lines changed: 24 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@ export interface NavigationContext {
2424
}
2525

2626
/**
27-
* IdP hosts that hard-block OAuth inside embedded user agents. Navigation to
28-
* these hosts is cancelled and rerouted: from an auth surface the app starts
29-
* the system-browser login handoff; from anywhere else it offers to finish the
30-
* integration connect in the browser (tokens land server-side either way).
27+
* IdP hosts that hard-block OAuth inside embedded user agents. An integration
28+
* connect heading for one of these is cancelled and offered as a finish-in-the-
29+
* browser flow instead (tokens land server-side either way). Sign-in never
30+
* consults this list — see {@link classifyNavigation}.
3131
*/
3232
export const SYSTEM_BROWSER_IDP_HOSTS: readonly string[] = [
3333
'accounts.google.com',
@@ -38,14 +38,6 @@ export const SYSTEM_BROWSER_IDP_HOSTS: readonly string[] = [
3838
'sts.windows.net',
3939
]
4040

41-
/**
42-
* IdP hosts verified lenient toward embedded user agents (the U5 provider
43-
* matrix). Only consulted for navigations leaving an auth surface — everywhere
44-
* else unknown hosts already stay in-window because same-window departures
45-
* from workspace pages are OAuth connect flows in this app.
46-
*/
47-
export const IN_WINDOW_IDP_HOSTS: readonly string[] = ['github.com']
48-
4941
const AUTH_SURFACE_PREFIXES: readonly string[] = [
5042
'/login',
5143
'/signup',
@@ -101,11 +93,21 @@ export function isAuthSurfacePath(pathname: string): boolean {
10193
* Classifies a top-level navigation (will-navigate / will-redirect).
10294
*
10395
* Same-window departures to non-origin hosts are OAuth flows in this app —
104-
* regular external links always go through window.open — so unknown hosts stay
105-
* in-window (lenient IdP assumption) except for the known embedded-blocking
106-
* hosts, which are rerouted through the system browser. Departures from auth
107-
* surfaces default to the system browser because SSO IdPs need real-browser
108-
* device claims.
96+
* regular external links always go through window.open.
97+
*
98+
* A departure from an auth surface is a *sign-in*, and sign-in always runs in
99+
* the system browser (RFC 8252), whatever the IdP. Embedding one is a one-way
100+
* door: the shell has no browser chrome, so a user who reaches the IdP and
101+
* decides not to continue has no way back to the app. It also splits the flow
102+
* across two cookie jars — better-auth binds its OAuth state cookie to the user
103+
* agent that started the flow, so an in-window start that finishes anywhere
104+
* else comes back `state_mismatch`. The handoff keeps the whole flow in one
105+
* jar and hands a one-time token back over the loopback.
106+
*
107+
* Everything else is an integration connect from a workspace page: those stay
108+
* in-window (the session cookie is already in this partition) unless the IdP
109+
* hard-blocks embedded user agents, which is what {@link
110+
* SYSTEM_BROWSER_IDP_HOSTS} enumerates.
109111
*/
110112
export function classifyNavigation(rawUrl: string, ctx: NavigationContext): MainNavigationAction {
111113
if (rawUrl === 'about:blank') {
@@ -125,16 +127,13 @@ export function classifyNavigation(rawUrl: string, ctx: NavigationContext): Main
125127
const fromAuthSurface = current
126128
? current.origin === ctx.appOrigin && isAuthSurfacePath(current.pathname)
127129
: false
128-
if (matchesHostList(url.hostname, SYSTEM_BROWSER_IDP_HOSTS)) {
129-
return fromAuthSurface ? 'idp-system-login' : 'idp-system-connect'
130+
if (fromAuthSurface) {
131+
return 'idp-system-login'
130132
}
131-
if (matchesHostList(url.hostname, IN_WINDOW_IDP_HOSTS)) {
132-
return 'idp-in-window'
133-
}
134-
if (current && current.origin !== ctx.appOrigin) {
135-
return 'idp-in-window'
133+
if (matchesHostList(url.hostname, SYSTEM_BROWSER_IDP_HOSTS)) {
134+
return 'idp-system-connect'
136135
}
137-
return fromAuthSurface ? 'idp-system-login' : 'idp-in-window'
136+
return 'idp-in-window'
138137
}
139138

140139
/**

0 commit comments

Comments
 (0)