GeezerVison is showing github.com — Merge remote-tracking branch 'origin/staging' into feat-enhance-cmd-k · simstudioai/sim@9ad2d36
File treeExpand file treeCollapse file tree Open diff view settings app/workspace/[workspaceId] components/mothership-chat
w/[workflowId]/components/panel
Some content is hiddenLarge Commits have some content hidden by default. Use the searchbox below for content that may be hidden. Dismiss bannerExpand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: README.md +1Lines changed: 1 addition & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -105,6 +105,7 @@ Manage your install with `bun run sim`: | 105 | 105 |
| 106 | 106 | ```bash | 107 | 107 | bun run sim start | stop | restart # bring your install up / down / cycle | | 108 | +bun run sim update # pull/rebuild and apply Compose images | 108 | 109 | bun run sim status # what's installed and healthy | 109 | 110 | bun run sim logs # follow logs | 110 | 111 | bun run sim doctor # diagnose configuration problems | |
Collapse file Copy file name to clipboardExpand all lines: apps/docs/content/docs/en/integrations/incidentio.mdx +640Lines changed: 640 additions & 0 deletions - Display the source diff
- Display the rich diff
Large diffs are not rendered by default. Collapse file Copy file name to clipboardExpand all lines: apps/docs/content/docs/en/integrations/salesforce-service-account.mdx +23-6Lines changed: 23 additions & 6 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -56,7 +56,7 @@ External Client Apps are Salesforce's current-generation connected apps and the | 56 | 56 | {/* TODO(screenshot): New External Client App form with the basic details filled in */} | 57 | 57 | </Step> | 58 | 58 | <Step> | 59 | | - In the OAuth settings section, enable OAuth (exact labels vary by Salesforce release) | | 59 | + Open the app's **Edit Settings** → **API (Enable OAuth Settings)** section and turn on **Enable OAuth** | 60 | 60 | </Step> | 61 | 61 | <Step> | 62 | 62 | Enter any placeholder **Callback URL** (e.g. `https://login.salesforce.com/services/oauth2/callback`) — it's required by the form but unused by this flow | @@ -81,7 +81,7 @@ External Client Apps are Salesforce's current-generation connected apps and the | 81 | 81 | Open your app in **External Client App Manager** and edit its **Policies** | 82 | 82 | </Step> | 83 | 83 | <Step> | 84 | | - Under **OAuth Policies** → **Client Credentials Flow**, check **Enable Client Credentials Flow** and set **Run As** to the integration user from step 1, then save | | 84 | + Under **OAuth Policies** → **Client Credentials Flow**, set **Run As** to the integration user from step 1, then save. That page holds only the Run As picker — the **Enable Client Credentials Flow** checkbox itself lives in **Edit Settings** → **OAuth Settings**, where you set it in step 2 | 85 | 85 |
| 86 | 86 | {/* TODO(screenshot): OAuth Policies with the Run As integration user set under Client Credentials Flow */} | 87 | 87 | </Step> | @@ -133,9 +133,13 @@ The JWT Bearer Flow authenticates with an uploaded certificate rather than a sha | 133 | 133 | ``` | 134 | 134 |
| 135 | 135 | Keep `server.key` somewhere safe — Sim stores it encrypted and never shows it again. | | 136 | + | | 137 | + <Callout type="warn"> | | 138 | + Salesforce requires an **RSA** key of at least 2048 bits for this flow; an ECDSA key is rejected. The uploaded certificate must also stay under 4 KB, which a 2048-bit self-signed cert comfortably is. | | 139 | + </Callout> | 136 | 140 | </Step> | 137 | 141 | <Step> | 138 | | - In **External Client App Manager** → your app → **Edit Settings** → **OAuth Settings**, enable the **JWT Bearer Flow** toggle and upload `server.crt`. On a legacy Connected App the equivalent is **Use digital signatures** with the same file | | 142 | + In **External Client App Manager** → your app → **Edit Settings** → **OAuth Settings**, turn on **Enable OAuth** (the JWT toggle does not appear until it is on), then **Enable JWT Bearer Flow**, and use **Upload Files** to upload `server.crt`. On a legacy Connected App the equivalent is **Use digital signatures** with the same file | 139 | 143 | </Step> | 140 | 144 | <Step> | 141 | 145 | Edit the app's **Policies** → **OAuth Policies** and set **Permitted Users** to **Admin approved users are pre-authorized** | @@ -145,7 +149,11 @@ The JWT Bearer Flow authenticates with an uploaded certificate rather than a sha | 145 | 149 | </Callout> | 146 | 150 | </Step> | 147 | 151 | <Step> | 148 | | - Assign the integration user's **profile** or a **permission set** to the app (External Client App Manager → your app → **Policies** → manage profiles/permission sets), so that user is covered by the pre-authorization | | 152 | + Assign the integration user's **profile** or a **permission set** to the app, so that user is covered by the pre-authorization | | 153 | + | | 154 | + <Callout type="warn"> | | 155 | + Assign it through the **profile**, or through a **second permission set that does _not_ carry the Salesforce API Integration permission set license**. A permission set backed by that license cannot hold an **Assigned Connected Apps** / **Assigned External Client Apps** section at all, so the app can never be assigned from the same permission set that grants the user its object access. Getting this wrong produces exactly the `user hasn't approved this consumer` failure this step exists to prevent. | | 156 | + </Callout> | 149 | 157 | </Step> | 150 | 158 | <Step> | 151 | 159 | Copy the **Consumer Key** as in step 4. There is no consumer secret to copy — the JWT flow doesn't use one | @@ -158,7 +166,7 @@ Go to **Setup** and search for **My Domain**. The host is required — Salesforc | 158 | 166 |
| 159 | 167 | - **Production:** `yourorg.my.salesforce.com` | 160 | 168 | - **Sandbox:** `yourorg--sandboxname.sandbox.my.salesforce.com` | 161 | | -- **Developer Edition:** `yourorg-dev-ed.develop.my.salesforce.com` | | 169 | +- **Developer Edition:** `yourorg.develop.my.salesforce.com` (Salesforce appends `-dev-ed` only when it generated the name for you) | 162 | 170 |
| 163 | 171 | Sim also accepts other partitioned My Domain hosts (`scratch`, `demo`, `patch`, `trailblaze`, `free`). | 164 | 172 |
| @@ -176,6 +184,15 @@ There's no scope picking beyond the **api** and **openid** scopes on the app — | 176 | 184 |
| 177 | 185 | A permissions gap surfaces at run time as a Salesforce API error; fix it on the integration user's permission sets — no changes are needed in Sim. | 178 | 186 |
| | 187 | +<Callout type="warn"> | | 188 | +On the **Salesforce Integration** (API-only) license specifically, SOQL and CRUD on standard objects are well supported, but two areas are not safe to assume: | | 189 | + | | 190 | +- **Reports and dashboards** are unverified on this license. Salesforce documents neither a grant nor a prohibition. Test them in a sandbox before depending on them, and remember the user also needs access to the report or dashboard **folder**. | | 191 | +- **Anything Apex-related is blocked** — Apex Class Access is one of the permissions this license cannot hold, so Tooling API calls touching `ApexClass` will fail. Custom field and custom object management is unaffected. | | 192 | + | | 193 | +If a workflow must run reports, a standard-seat integration user is the safe choice. | | 194 | +</Callout> | | 195 | + | 179 | 196 | ## Adding the Service Account to Sim | 180 | 197 |
| 181 | 198 | <Steps> | @@ -230,7 +247,7 @@ Deactivating or freezing the integration user — the Run As user for client cre | 230 | 247 | { question: "Setup → App Manager → New Connected App is greyed out — why?", answer: "Salesforce disabled new Connected App creation by default (new orgs since Summer '25, all orgs since Spring '26); re-enabling it requires a Salesforce Support request. You don't need it — create an External Client App instead (Setup → External Client App Manager), which supports the same Client Credentials Flow and produces the same consumer key and secret." }, | 231 | 248 | { question: "Why do I have to enter a My Domain host instead of login.salesforce.com?", answer: "Salesforce only supports the Client Credentials Flow at your org's My Domain URL — login.salesforce.com and test.salesforce.com are explicitly rejected for this flow. Find yours under Setup → My Domain." }, | 232 | 249 | { question: "The credential validates but a specific tool fails with a permission error — why?", answer: "Every call runs as the Run As user, so the failing tool needs a permission that user doesn't have. Common gaps: Customize Application for custom field/object tools, Run Reports plus folder access for report tools, and object or field permissions for the records involved. Fix it on the integration user's permission sets and re-run." }, | 233 | | - { question: "Does this work with sandboxes and Developer Edition orgs?", answer: "Yes — enter the sandbox host (yourorg--sandboxname.sandbox.my.salesforce.com) or Developer Edition host (yourorg-dev-ed.develop.my.salesforce.com) as the My Domain host. Government/military domains (*.my.salesforce.mil) aren't currently supported." }, | | 250 | + { question: "Does this work with sandboxes and Developer Edition orgs?", answer: "Yes — enter the sandbox host (yourorg--sandboxname.sandbox.my.salesforce.com) or Developer Edition host (yourorg.develop.my.salesforce.com, with a -dev-ed suffix only if Salesforce generated the name for you) as the My Domain host. Government/military domains (*.my.salesforce.mil) aren't currently supported." }, | 234 | 251 | { question: "How do I rotate the credentials?", answer: "For client credentials, rotate the Consumer Secret from the app's OAuth settings (External Client App: Settings → OAuth Settings → Consumer Key and Secret; legacy Connected App: Manage Consumer Details), then update the credential in Sim with the new secret. For JWT bearer, generate a new key pair, upload the new certificate to the app, and paste the new private key into Sim. Either way, reconnecting asks you to restate the authentication method — Sim never returns a stored secret, so it cannot pre-select it for you." }, | 235 | 252 | { question: "What happens if the integration user is deactivated or frozen?", answer: "All token minting fails with invalid_grant and every workflow using the credential stops. Reactivate the user (or point Run As at a new integration user) and runs resume — no changes needed in Sim." }, | 236 | 253 | ]} /> |
Collapse file Copy file name to clipboardExpand all lines: apps/docs/content/docs/en/platform/self-hosting/docker.mdx +2-3Lines changed: 2 additions & 3 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -149,8 +149,8 @@ docker compose -f docker-compose.prod.yml logs migrations | 149 | 149 | # Is the scheduler firing? | 150 | 150 | docker compose -f docker-compose.prod.yml logs -f cron | 151 | 151 |
| 152 | | -# Upgrade: bump SIM_VERSION in .env, then | 153 | | -docker compose -f docker-compose.prod.yml pull && docker compose -f docker-compose.prod.yml up -d | | 152 | +# Upgrade: bump SIM_VERSION in .env when pinned, then | | 153 | +bun run sim update | 154 | 154 | ``` | 155 | 155 |
| 156 | 156 | <FAQ items={[ | @@ -159,4 +159,3 @@ docker compose -f docker-compose.prod.yml pull && docker compose -f docker-compo | 159 | 159 | { question: "How do I back up and restore the database?", answer: "Back up with: docker compose -f docker-compose.prod.yml exec db pg_dump -U postgres simstudio > backup.sql. Restore with: docker compose -f docker-compose.prod.yml exec -T db psql -U postgres simstudio < backup.sql. The database data is persisted in a Docker volume named postgres_data."}, | 160 | 160 | { question: "Can I customize the PostgreSQL credentials?", answer: "Yes. The docker-compose.prod.yml uses environment variable defaults: POSTGRES_USER (default: postgres), POSTGRES_PASSWORD (default: postgres), POSTGRES_DB (default: simstudio), and POSTGRES_PORT (default: 5432). Set these in your .env file to override them." }, | 161 | 161 | ]} /> | 162 | | - |
Collapse file Copy file name to clipboardExpand all lines: apps/docs/content/docs/en/platform/self-hosting/upgrades.mdx +5-2Lines changed: 5 additions & 2 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -142,11 +142,14 @@ kubectl logs -n simstudio deploy/sim-app -c migrations --tail=100 | 142 | 142 | <Tab value="Docker Compose"> | 143 | 143 |
| 144 | 144 | ```bash | 145 | | -docker compose -f docker-compose.prod.yml pull | 146 | | -docker compose -f docker-compose.prod.yml up -d | | 145 | +bun run sim update | 147 | 146 | docker compose -f docker-compose.prod.yml logs migrations | 148 | 147 | ``` | 149 | 148 |
| | 149 | +`bun run sim update` pulls the versions configured by `SIM_VERSION` (or `latest` when it is | | 150 | +unset), recreates the changed services, and keeps data volumes. It is equivalent to running | | 151 | +`docker compose pull` followed by `docker compose up -d`. | | 152 | + | 150 | 153 | There is a short window where the app is unavailable while containers restart. Compose has no rolling-update mechanism — plan a maintenance window, or run Kubernetes if you need zero-downtime upgrades. | 151 | 154 |
| 152 | 155 | </Tab> | |
Collapse file Copy file name to clipboardExpand all lines: apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx +37-13Lines changed: 37 additions & 13 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -29,6 +29,7 @@ import { | 29 | 29 | parseLastCredentialTag, | 30 | 30 | parseLastQuestionTag, | 31 | 31 | } from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags' | | 32 | +import { nextSizerFloor } from '@/app/workspace/[workspaceId]/home/components/mothership-chat/sizer-floor' | 32 | 33 | import { QueuedMessages } from '@/app/workspace/[workspaceId]/home/components/queued-messages' | 33 | 34 | import { | 34 | 35 | UserInput, | @@ -328,6 +329,8 @@ export function MothershipChat({ | 328 | 329 | const sizerRef = useRef<HTMLDivElement | null>(null) | 329 | 330 | const scrollerPaddingRef = useRef<{ top: number; bottom: number } | null>(null) | 330 | 331 | const sizerFloorAppliedRef = useRef(0) | | 332 | + const heldHighWaterRef = useRef(0) | | 333 | + const floorChatRef = useRef<string | undefined>(undefined) | 331 | 334 | const floorDrainRafRef = useRef(0) | 332 | 335 | useEffect(() => () => cancelAnimationFrame(floorDrainRafRef.current), []) | 333 | 336 |
| @@ -337,11 +340,11 @@ export function MothershipChat({ | 337 | 340 | * row-height shrinks; when they pull scrollHeight under | 338 | 341 | * `scrollTop + clientHeight`, the browser clamps `scrollTop` and the pinned | 339 | 342 | * transcript visibly drops, then the chase glides it back. Flooring the | 340 | | - * sizer at exactly the scrolled-to extent prevents that clamp while never | 341 | | - * ADDING space — the floor cannot exceed what is already on screen. So an | 342 | | - * estimate correction (a fresh row measuring smaller than | 343 | | - * ROW_HEIGHT_ESTIMATE) releases immediately instead of holding phantom space | 344 | | - * the chase would scroll into and bounce back out of. | | 343 | + * sizer prevents that clamp while never ADDING space, so an estimate | | 344 | + * correction (a fresh row measuring smaller than ROW_HEIGHT_ESTIMATE) | | 345 | + * releases immediately instead of holding phantom space the chase would | | 346 | + * scroll into and bounce back out of. {@link nextSizerFloor} owns the value | | 347 | + * and the invariant that keeps it honest. | 345 | 348 | * | 346 | 349 | * Active on the same signal as auto-scroll: the reveal keeps re-parsing | 347 | 350 | * markdown (and shrinking) after the network stream closes, so the floor | @@ -361,7 +364,26 @@ export function MothershipChat({ | 361 | 364 | const sizer = sizerRef.current | 362 | 365 | const el = scrollElementRef.current | 363 | 366 | if (!sizer || !el) return | | 367 | + // A chat switch replaces the entire transcript, so a floor held for the | | 368 | + // previous one is meaningless — and its high-water mark would otherwise | | 369 | + // hand a short chat the tall chat's space for as long as the outgoing | | 370 | + // turn's `lastRowAnimating` keeps the floor engaged. Released outright | | 371 | + // rather than drained: the switch re-lands the viewport anyway, so there | | 372 | + // is no eased settle to preserve. A pending chat adopting its id is the | | 373 | + // SAME conversation, so it must not release mid-turn. | | 374 | + if (floorChatRef.current !== chatId) { | | 375 | + const isPendingPersist = floorChatRef.current === undefined && chatId !== undefined | | 376 | + floorChatRef.current = chatId | | 377 | + if (!isPendingPersist) { | | 378 | + cancelAnimationFrame(floorDrainRafRef.current) | | 379 | + floorDrainRafRef.current = 0 | | 380 | + sizerFloorAppliedRef.current = 0 | | 381 | + heldHighWaterRef.current = 0 | | 382 | + sizer.style.minHeight = '' | | 383 | + } | | 384 | + } | 364 | 385 | if (!floorActive) { | | 386 | + heldHighWaterRef.current = 0 | 365 | 387 | if (sizerFloorAppliedRef.current === 0) return | 366 | 388 | // A drain already in flight keeps its own rAF cadence — settle-burst | 367 | 389 | // commits re-enter this branch and must not add extra steps in layout, | @@ -405,14 +427,16 @@ export function MothershipChat({ | 405 | 427 | } | 406 | 428 | } | 407 | 429 | const padding = scrollerPaddingRef.current | 408 | | - // Math.floor, not the raw float: a fractional min-height can round | 409 | | - // scrollHeight 1px ABOVE the scrolled-to extent, and that phantom 1px gap | 410 | | - // re-derives 1px higher after every chase step — a visible 1px/frame | 411 | | - // upward creep whenever the floor is what's holding scrollHeight. | 412 | | - const floor = Math.max( | 413 | | - 0, | 414 | | - Math.floor(el.scrollTop + el.clientHeight - padding.top - padding.bottom) | 415 | | - ) | | 430 | + const { floor, highWater } = nextSizerFloor({ | | 431 | + previousHighWater: heldHighWaterRef.current, | | 432 | + appliedFloor: sizerFloorAppliedRef.current, | | 433 | + contentHeight: virtualizer.getTotalSize(), | | 434 | + scrollTop: el.scrollTop, | | 435 | + clientHeight: el.clientHeight, | | 436 | + paddingTop: padding.top, | | 437 | + paddingBottom: padding.bottom, | | 438 | + }) | | 439 | + heldHighWaterRef.current = highWater | 416 | 440 | // Dead-band: the floor feeds back into its own inputs (a floored value can | 417 | 441 | // land a fraction BELOW the extent, the browser clamps scrollTop, and the | 418 | 442 | // next commit re-derives from the clamped position — a visible ~1px×N | |
Collapse file Copy file name to clipboard +90Lines changed: 90 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +/** | | 2 | + * @vitest-environment node | | 3 | + */ | | 4 | +import { describe, expect, it } from 'vitest' | | 5 | +import { nextSizerFloor } from '@/app/workspace/[workspaceId]/home/components/mothership-chat/sizer-floor' | | 6 | + | | 7 | +/** Matches the transcript scroller's `pt-4 pb-2`. */ | | 8 | +const PADDING = { paddingTop: 16, paddingBottom: 8 } | | 9 | +/** A viewport scrolled to the bottom of 2000px of content: extent resolves to 2000. */ | | 10 | +const PINNED = { scrollTop: 1424, clientHeight: 600, ...PADDING } | | 11 | + | | 12 | +describe('nextSizerFloor', () => { | | 13 | + it('follows the scrolled-to extent while it stays under the high-water mark', () => { | | 14 | + const { floor } = nextSizerFloor({ | | 15 | + ...PINNED, | | 16 | + previousHighWater: 5000, | | 17 | + appliedFloor: 0, | | 18 | + contentHeight: 5000, | | 19 | + }) | | 20 | + expect(floor).toBe(2000) | | 21 | + }) | | 22 | + | | 23 | + it('floors fractional scroll offsets down so the sizer cannot round above the extent', () => { | | 24 | + const { floor } = nextSizerFloor({ | | 25 | + scrollTop: 100.7, | | 26 | + clientHeight: 600, | | 27 | + ...PADDING, | | 28 | + previousHighWater: 5000, | | 29 | + appliedFloor: 0, | | 30 | + contentHeight: 5000, | | 31 | + }) | | 32 | + expect(floor).toBe(676) | | 33 | + }) | | 34 | + | | 35 | + it('never returns a negative floor for a container smaller than its padding', () => { | | 36 | + const { floor } = nextSizerFloor({ | | 37 | + scrollTop: 0, | | 38 | + clientHeight: 8, | | 39 | + ...PADDING, | | 40 | + previousHighWater: 500, | | 41 | + appliedFloor: 0, | | 42 | + contentHeight: 500, | | 43 | + }) | | 44 | + expect(floor).toBe(0) | | 45 | + }) | | 46 | + | | 47 | + it('never exceeds the content height when the transcript is shorter than the viewport', () => { | | 48 | + const { floor } = nextSizerFloor({ | | 49 | + scrollTop: 0, | | 50 | + clientHeight: 600, | | 51 | + ...PADDING, | | 52 | + previousHighWater: 0, | | 53 | + appliedFloor: 0, | | 54 | + contentHeight: 180, | | 55 | + }) | | 56 | + expect(floor).toBe(180) | | 57 | + }) | | 58 | + | | 59 | + it('holds the high-water mark when content re-measures smaller mid-turn', () => { | | 60 | + const { floor, highWater } = nextSizerFloor({ | | 61 | + ...PINNED, | | 62 | + previousHighWater: 2000, | | 63 | + appliedFloor: 2000, | | 64 | + contentHeight: 1940, | | 65 | + }) | | 66 | + expect(highWater).toBe(2000) | | 67 | + expect(floor).toBe(2000) | | 68 | + }) | | 69 | + | | 70 | + it('carries undrained debt across a turn boundary that interrupts the drain', () => { | | 71 | + const { floor, highWater } = nextSizerFloor({ | | 72 | + ...PINNED, | | 73 | + previousHighWater: 0, | | 74 | + appliedFloor: 1985, | | 75 | + contentHeight: 1940, | | 76 | + }) | | 77 | + expect(highWater).toBe(1985) | | 78 | + expect(floor).toBe(1985) | | 79 | + }) | | 80 | + | | 81 | + it('raises the high-water mark as content grows', () => { | | 82 | + const { highWater } = nextSizerFloor({ | | 83 | + ...PINNED, | | 84 | + previousHighWater: 1200, | | 85 | + appliedFloor: 1200, | | 86 | + contentHeight: 1600, | | 87 | + }) | | 88 | + expect(highWater).toBe(1600) | | 89 | + }) | | 90 | +}) |
|