Skip to content

Commit 9ad2d36

Browse files
committed
Merge remote-tracking branch 'origin/staging' into feat-enhance-cmd-k
# Conflicts: # apps/sim/app/workspace/[workspaceId]/home/home.tsx # apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.mount-send.test.tsx # apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts # apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx # apps/sim/lib/core/utils/browser-storage.ts # apps/sim/lib/mothership/events.ts
2 parents 61a2559 + 1551923 commit 9ad2d36

63 files changed

Lines changed: 5843 additions & 128 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ Manage your install with `bun run sim`:
105105

106106
```bash
107107
bun run sim start | stop | restart # bring your install up / down / cycle
108+
bun run sim update # pull/rebuild and apply Compose images
108109
bun run sim status # what's installed and healthy
109110
bun run sim logs # follow logs
110111
bun run sim doctor # diagnose configuration problems

apps/docs/content/docs/en/integrations/incidentio.mdx

Lines changed: 640 additions & 0 deletions
Large diffs are not rendered by default.

apps/docs/content/docs/en/integrations/salesforce-service-account.mdx

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ External Client Apps are Salesforce's current-generation connected apps and the
5656
{/* TODO(screenshot): New External Client App form with the basic details filled in */}
5757
</Step>
5858
<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**
6060
</Step>
6161
<Step>
6262
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
8181
Open your app in **External Client App Manager** and edit its **Policies**
8282
</Step>
8383
<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
8585

8686
{/* TODO(screenshot): OAuth Policies with the Run As integration user set under Client Credentials Flow */}
8787
</Step>
@@ -133,9 +133,13 @@ The JWT Bearer Flow authenticates with an uploaded certificate rather than a sha
133133
```
134134

135135
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>
136140
</Step>
137141
<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
139143
</Step>
140144
<Step>
141145
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
145149
</Callout>
146150
</Step>
147151
<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>
149157
</Step>
150158
<Step>
151159
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
158166

159167
- **Production:** `yourorg.my.salesforce.com`
160168
- **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)
162170

163171
Sim also accepts other partitioned My Domain hosts (`scratch`, `demo`, `patch`, `trailblaze`, `free`).
164172

@@ -176,6 +184,15 @@ There's no scope picking beyond the **api** and **openid** scopes on the app —
176184

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

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+
179196
## Adding the Service Account to Sim
180197

181198
<Steps>
@@ -230,7 +247,7 @@ Deactivating or freezing the integration user — the Run As user for client cre
230247
{ 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." },
231248
{ 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." },
232249
{ 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." },
234251
{ 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." },
235252
{ 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." },
236253
]} />

apps/docs/content/docs/en/platform/self-hosting/docker.mdx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,8 +149,8 @@ docker compose -f docker-compose.prod.yml logs migrations
149149
# Is the scheduler firing?
150150
docker compose -f docker-compose.prod.yml logs -f cron
151151

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
154154
```
155155

156156
<FAQ items={[
@@ -159,4 +159,3 @@ docker compose -f docker-compose.prod.yml pull && docker compose -f docker-compo
159159
{ 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."},
160160
{ 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." },
161161
]} />
162-

apps/docs/content/docs/en/platform/self-hosting/upgrades.mdx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,11 +142,14 @@ kubectl logs -n simstudio deploy/sim-app -c migrations --tail=100
142142
<Tab value="Docker Compose">
143143

144144
```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
147146
docker compose -f docker-compose.prod.yml logs migrations
148147
```
149148

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+
150153
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.
151154

152155
</Tab>

apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
parseLastCredentialTag,
3030
parseLastQuestionTag,
3131
} from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags'
32+
import { nextSizerFloor } from '@/app/workspace/[workspaceId]/home/components/mothership-chat/sizer-floor'
3233
import { QueuedMessages } from '@/app/workspace/[workspaceId]/home/components/queued-messages'
3334
import {
3435
UserInput,
@@ -328,6 +329,8 @@ export function MothershipChat({
328329
const sizerRef = useRef<HTMLDivElement | null>(null)
329330
const scrollerPaddingRef = useRef<{ top: number; bottom: number } | null>(null)
330331
const sizerFloorAppliedRef = useRef(0)
332+
const heldHighWaterRef = useRef(0)
333+
const floorChatRef = useRef<string | undefined>(undefined)
331334
const floorDrainRafRef = useRef(0)
332335
useEffect(() => () => cancelAnimationFrame(floorDrainRafRef.current), [])
333336

@@ -337,11 +340,11 @@ export function MothershipChat({
337340
* row-height shrinks; when they pull scrollHeight under
338341
* `scrollTop + clientHeight`, the browser clamps `scrollTop` and the pinned
339342
* 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.
345348
*
346349
* Active on the same signal as auto-scroll: the reveal keeps re-parsing
347350
* markdown (and shrinking) after the network stream closes, so the floor
@@ -361,7 +364,26 @@ export function MothershipChat({
361364
const sizer = sizerRef.current
362365
const el = scrollElementRef.current
363366
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+
}
364385
if (!floorActive) {
386+
heldHighWaterRef.current = 0
365387
if (sizerFloorAppliedRef.current === 0) return
366388
// A drain already in flight keeps its own rAF cadence — settle-burst
367389
// commits re-enter this branch and must not add extra steps in layout,
@@ -405,14 +427,16 @@ export function MothershipChat({
405427
}
406428
}
407429
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
416440
// Dead-band: the floor feeds back into its own inputs (a floored value can
417441
// land a fraction BELOW the extent, the browser clamps scrollTop, and the
418442
// next commit re-derives from the clamped position — a visible ~1px×N
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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+
})

0 commit comments

Comments
 (0)