Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
f69c624
fix(desktop): origin settings for prod, github login improvements (#6…
icecrasher321 Jul 29, 2026
e369997
fix(db): encode Date binds in raw sql, document the fetch_types contr…
TheodoreSpeaks Jul 29, 2026
6ff6255
feat(outlook): add Microsoft Graph calendar operations (#6041)
mzxchandra Jul 29, 2026
21c28d7
fix(tables): truncate sort option labels (#6052)
BillLeoutsakosvl346 Jul 29, 2026
0618541
fix(knowledge): change "Create Tag" to "Apply Tag" when applying a ta…
j15z Jul 29, 2026
985f9e6
feat(tables): saved views with filter, sort, and column presets (#5961)
TheodoreSpeaks Jul 29, 2026
ee7c061
fix(copilot): stop the special-tag parser discarding text it cannot r…
j15z Jul 29, 2026
4793607
chore(db): drop the legacy folder tables and adopt the deferred folde…
waleedlatif1 Jul 29, 2026
0f12799
fix(folders): stop archived rows skewing sortOrder, hide them from ad…
waleedlatif1 Jul 29, 2026
9edf892
docs(folders): correct four comments that claimed more than the code …
waleedlatif1 Jul 29, 2026
25d8019
feat(pi): Babysit foundations — shared PR/push extraction, five GitHu…
BillLeoutsakosvl346 Jul 29, 2026
4f776b7
fix(desktop): fix 11 security findings from a deepsec scan of the des…
waleedlatif1 Jul 29, 2026
1a23438
fix(desktop): release script (#6060)
Sg312 Jul 29, 2026
161b21f
feat(pi): require the user's own API key for Create PR even on hosted…
j15z Jul 30, 2026
998fd5a
fix(ci): scope the Next.js build cache sticky disk per branch (#6072)
waleedlatif1 Jul 30, 2026
0a0acc2
fix(config): drop three dead next.config entries and correct two stal…
waleedlatif1 Jul 30, 2026
f78367c
feat(logfire): add Pydantic Logfire block, tools, and docs (#6075)
icecrasher321 Jul 30, 2026
f0b79c5
chore(deps): upgrade next 16.2.11 -> 16.2.12 (#6077)
waleedlatif1 Jul 30, 2026
911b958
feat(exa): refresh Exa integration against current API, retire dead r…
waleedlatif1 Jul 30, 2026
4b239fa
fix(tests): stop pi-lifetime asserting the ceiling on a racing bounda…
waleedlatif1 Jul 30, 2026
94778eb
feat(pi): add update PR mode (#6031)
BillLeoutsakosvl346 Jul 30, 2026
897eebd
fix(blocks): stop registry.ts reading BLOCK_REGISTRY at module scope …
waleedlatif1 Jul 30, 2026
3e62546
perf(ci): disable the Turbopack persistent build cache (3.2x faster b…
waleedlatif1 Jul 30, 2026
18ddc22
feat(library): Best AI Agents for Scheduling and Calendar Management …
icecrasher321 Jul 30, 2026
d67e02f
perf(tests): skip jest-dom in node-environment test files (#6088)
waleedlatif1 Jul 30, 2026
8260891
chore(gitignore): ignore the repo-root uploads spill directory (#6089)
waleedlatif1 Jul 30, 2026
11c0d3b
feat(organizations): sweep a joiner's owned workspaces into the org o…
icecrasher321 Jul 30, 2026
edef789
feat(desktop): floating hover-peek sidebar (#6086)
waleedlatif1 Jul 30, 2026
1ce2aef
fix(triggers): stop cloned trigger blocks reusing the source's webhoo…
waleedlatif1 Jul 30, 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
6 changes: 5 additions & 1 deletion .github/workflows/test-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,11 @@ jobs:
build:
name: Build App
runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-16vcpu-ubuntu-2404' || 'linux-x64-8-core' }}
timeout-minutes: 15
# Build durations crossed 15 minutes as the app grew (10m02 on Jul 29 AM,
# 14m44 after the folders/desktop/library merges, then two straight
# timeouts) — GitHub reports a job timeout as "cancelled". 25 keeps
# headroom without masking a genuine hang.
timeout-minutes: 25

steps:
- name: Checkout code
Expand Down
6 changes: 3 additions & 3 deletions apps/desktop/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Sim Desktop (macOS)

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.
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.

## Layout

Expand Down Expand Up @@ -44,7 +44,7 @@ e2e/ # Playwright _electron smoke suite
```bash
bun install # workspace root
cd apps/desktop
bun run dev # bundle + launch against https://sim.ai
bun run dev # bundle + launch against https://www.sim.ai
SIM_DESKTOP_ORIGIN=http://localhost:3000 bun run dev # against local sim
```

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

## Provider matrix (U5 spike — keep current)

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.
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.

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

Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/scripts/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const watch = process.argv.includes('--watch')
// non-prod environment): SIM_DESKTOP_DEFAULT_ORIGIN=https://www.dev.sim.ai.
// Baked into the bundle so it applies to fresh installs with no settings —
// unlike the SIM_DESKTOP_ORIGIN env var, which only affects terminal-launched
// processes. Official builds leave it unset (default https://sim.ai).
// processes. Official builds leave it unset (default https://www.sim.ai).
const bakedDefaultOrigin = process.env.SIM_DESKTOP_DEFAULT_ORIGIN ?? ''
if (
bakedDefaultOrigin &&
Expand Down
8 changes: 5 additions & 3 deletions apps/desktop/scripts/install-local.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,11 @@ const CHANNEL_FLAGS: Record<string, ChannelIdentity> = {
appId: 'ai.sim.desktop.staging',
origin: 'https://www.staging.sim.ai',
},
// Bare sim.ai, matching official prod builds (config.ts) — www.sim.ai
// would land in a different cookie partition than a real install.
'--prod': { name: 'Sim', appId: 'ai.sim.desktop', origin: 'https://sim.ai' },
// Origin left unset, exactly as official prod builds do, so this install
// resolves the same DEFAULT_ORIGIN (config.ts) and therefore the same cookie
// partition as a real one. Naming an origin here re-creates the drift that
// pinned prod to the apex while the server serves www.
'--prod': { name: 'Sim', appId: 'ai.sim.desktop' },
}

const DEFAULT_IDENTITY: ChannelIdentity = { name: 'Sim', appId: 'ai.sim.desktop' }
Expand Down
120 changes: 120 additions & 0 deletions apps/desktop/src/main/browser-agent/page-functions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,20 @@ describe('secret-field detection', () => {
],
['new-password field', '<input type="text" autocomplete="new-password" />'],
['uppercase autocomplete token', '<input type="text" autocomplete="Current-Password" />'],
// The spec allows space-separated detail tokens and WebAuthn recommends
// this exact value, so whole-string equality missed it.
[
'WebAuthn multi-token autocomplete',
'<input type="text" autocomplete="current-password webauthn" />',
],
[
'section-scoped autocomplete',
'<input type="text" autocomplete="section-login current-password" />',
],
[
'multi-token new-password with surrounding whitespace',
'<input type="text" autocomplete=" new-password webauthn " />',
],
]

it.each(secretCases)('clickElement refuses a %s', (_label, html) => {
Expand Down Expand Up @@ -276,6 +290,24 @@ describe('collectSnapshot', () => {
expect(outline).not.toContain('value=')
})

it.each([
['a one-time code', 'one-time-code', '123456'],
['a card number', 'cc-number', '4111111111111111'],
['a card security code', 'cc-csc', '737'],
['a card expiry', 'cc-exp', '12/29'],
])('withholds the value of %s while still listing the field', (_label, token, value) => {
document.body.innerHTML = `<input type="text" autocomplete="${token}" value="${value}" aria-label="Field" />`
visible(document.querySelector('input') as HTMLInputElement)

const outline = outlineOf(collectSnapshot())

// Not reported as a password-field: the agent must still be able to fill
// these, it just never learns what is already there.
expect(outline).not.toContain('password-field')
expect(outline).not.toContain(value)
expect(outline).toContain('value-withheld')
})

it('withholds the value of a revealed password field', () => {
document.body.innerHTML =
'<input type="text" autocomplete="current-password" value="hunter2" aria-label="Password" />'
Expand Down Expand Up @@ -317,6 +349,25 @@ describe('readActiveElementState', () => {
expect(readActiveElementState()).toMatchObject({ redacted: true, valuePreview: '' })
})

it.each([
['a one-time code', 'one-time-code', '123456'],
['a card number', 'cc-number', '4111111111111111'],
['a card security code', 'cc-csc', '737'],
])('withholds %s on readback but still confirms the fill', (_label, token, value) => {
document.body.innerHTML = `<input type="text" autocomplete="${token}" value="${value}" />`
setActiveElement(document, document.querySelector('input'))

// valueLength is kept: without it a successful type reads as "still empty"
// and the agent types the code a second time.
expect(readActiveElementState()).toEqual({
activeElement: 'input',
selectedChars: 0,
valueLength: value.length,
valuePreview: '',
redacted: true,
})
})

it('reports ordinary fields in full', () => {
document.body.innerHTML = '<input type="text" value="tokyo" />'
setActiveElement(document, document.querySelector('input'))
Expand All @@ -343,6 +394,35 @@ describe('readActiveElementState', () => {
})
})

describe('XHTML lower-case tagName', () => {
/** An element whose tagName reads lower-case, as it does in an XHTML document. */
function lowerCaseTagInput(html: string): HTMLInputElement {
document.body.innerHTML = html
const input = document.querySelector('input') as HTMLInputElement
Object.defineProperty(input, 'tagName', { configurable: true, get: () => 'input' })
return input
}

it('still refuses a password field whose tagName is lower-case', () => {
const input = lowerCaseTagInput('<input type="password" />')
register(visible(input))

expect(typeIntoElement(0, 'hunter2', false)).toEqual({ error: 'password' })
expect(input.value).toBe('')
})

it('still withholds the value of a lower-case-tagName credential field', () => {
const input = lowerCaseTagInput(
'<input type="password" value="hunter2" aria-label="Password" />'
)
visible(input)

const outline = outlineOf(collectSnapshot())

expect(outline).not.toContain('hunter2')
})
})

describe('activeElementSecrecy', () => {
it('reports safe for an ordinary field', () => {
document.body.innerHTML = '<input type="text" />'
Expand Down Expand Up @@ -386,6 +466,46 @@ describe('activeElementSecrecy', () => {
expect(activeElementSecrecy()).toBe('opaque')
})

it('reports opaque for a password field inside a CLOSED shadow root', () => {
const host = document.createElement('div')
document.body.append(host)
const shadow = host.attachShadow({ mode: 'closed' })
shadow.innerHTML = '<input type="password" />'
// Focus inside a closed root retargets to the host and `shadowRoot` reads
// null, which is exactly what the browser reports and what made this 'safe'.
setActiveElement(document, host)

expect(host.shadowRoot).toBeNull()
expect(activeElementSecrecy()).toBe('opaque')
})

it('reports opaque for a closed shadow root on a custom element', () => {
const host = document.createElement('my-login')
document.body.append(host)
host.attachShadow({ mode: 'closed' }).innerHTML = '<input autocomplete="new-password" />'
setActiveElement(document, host)

expect(activeElementSecrecy()).toBe('opaque')
})

it('still reports safe for a focused element that is focusable in its own right', () => {
// The false-positive guard: a div the page made focusable is focused
// itself, not hiding a shadow tree, so keystrokes are not refused.
document.body.innerHTML = '<div tabindex="0">menu</div>'
setActiveElement(document, document.querySelector('div'))

expect(activeElementSecrecy()).toBe('safe')
})

it('still reports safe for a focused contenteditable', () => {
document.body.innerHTML = '<div contenteditable="true">note</div>'
const editable = document.querySelector('div') as HTMLElement
Object.defineProperty(editable, 'isContentEditable', { get: () => true })
setActiveElement(document, editable)

expect(activeElementSecrecy()).toBe('safe')
})

it('descends into a same-origin frame instead of calling it opaque', () => {
const frame = document.createElement('iframe')
document.body.append(frame)
Expand Down
Loading
Loading