Skip to content

Commit 72b749b

Browse files
Bill Leoutsakoscursoragent
authored andcommitted
Merge staging into e2e/settings-playwright
Co-authored-by: Cursor <cursoragent@cursor.com>
2 parents beba0c2 + 0199508 commit 72b749b

516 files changed

Lines changed: 15872 additions & 11098 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.

.agents/skills/add-block/SKILL.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,25 @@ export const {ServiceName}Block: BlockConfig = {
144144

145145
**Scope descriptions:** When adding a new OAuth provider, also add human-readable descriptions for all scopes in `SCOPE_DESCRIPTIONS` within `lib/oauth/utils.ts`.
146146

147+
**Service accounts (shared, app-level credentials):** A plain `oauth-input` already lets users *select* an existing service account — those credentials fold into the picker automatically (a Google service account created for any Google service appears in every Google block's picker). You only set `credentialKind` when you want to change the *connect* action:
148+
149+
```typescript
150+
{
151+
id: 'credential',
152+
title: 'Account',
153+
type: 'oauth-input',
154+
serviceId: '{service}',
155+
requiredScopes: getScopesForService('{service}'),
156+
credentialKind: 'any', // omit | 'service-account' | 'any'
157+
}
158+
```
159+
160+
- **omit (default):** lists OAuth accounts + any existing service accounts; the only connect action is "Connect account" (OAuth). Use this for the common "let users pick a service account someone set up elsewhere, but don't offer inline setup" case — no config needed.
161+
- **`'service-account'`:** service-account credentials *only*, plus an inline setup action that opens the provider's connect modal. Use when a block accepts *only* an app credential.
162+
- **`'any'`:** merged picker — OAuth accounts *and* service accounts in one grouped dropdown, with a connect action for each. Use when a block supports both (e.g. Slack: a personal account *or* a custom bot).
163+
164+
Optional companions: `credentialLabels` (override the picker's section/connect-row copy) and `allowServiceAccounts: true` (trigger-mode only — list service accounts, which triggers otherwise exclude; set only when the trigger's polling path can resolve a service-account token). The connect modal, provider families (Google JSON key, Atlassian token, token-paste, client-credential, Slack bot), and the preview gate are all resolved from `serviceAccountProviderId` — you don't wire them per block.
165+
147166
### Selectors (with dynamic options)
148167
```typescript
149168
// Channel selector (Slack, Discord, etc.)

.claude/commands/add-block.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,25 @@ export const {ServiceName}Block: BlockConfig = {
143143

144144
**Scope descriptions:** When adding a new OAuth provider, also add human-readable descriptions for all scopes in `SCOPE_DESCRIPTIONS` within `lib/oauth/utils.ts`.
145145

146+
**Service accounts (shared, app-level credentials):** A plain `oauth-input` already lets users *select* an existing service account — those credentials fold into the picker automatically (a Google service account created for any Google service appears in every Google block's picker). You only set `credentialKind` when you want to change the *connect* action:
147+
148+
```typescript
149+
{
150+
id: 'credential',
151+
title: 'Account',
152+
type: 'oauth-input',
153+
serviceId: '{service}',
154+
requiredScopes: getScopesForService('{service}'),
155+
credentialKind: 'any', // omit | 'service-account' | 'any'
156+
}
157+
```
158+
159+
- **omit (default):** lists OAuth accounts + any existing service accounts; the only connect action is "Connect account" (OAuth). Use this for the common "let users pick a service account someone set up elsewhere, but don't offer inline setup" case — no config needed.
160+
- **`'service-account'`:** service-account credentials *only*, plus an inline setup action that opens the provider's connect modal. Use when a block accepts *only* an app credential.
161+
- **`'any'`:** merged picker — OAuth accounts *and* service accounts in one grouped dropdown, with a connect action for each. Use when a block supports both (e.g. Slack: a personal account *or* a custom bot).
162+
163+
Optional companions: `credentialLabels` (override the picker's section/connect-row copy) and `allowServiceAccounts: true` (trigger-mode only — list service accounts, which triggers otherwise exclude; set only when the trigger's polling path can resolve a service-account token). The connect modal, provider families (Google JSON key, Atlassian token, token-paste, client-credential, Slack bot), and the preview gate are all resolved from `serviceAccountProviderId` — you don't wire them per block.
164+
146165
### Selectors (with dynamic options)
147166
```typescript
148167
// Channel selector (Slack, Discord, etc.)

.claude/rules/sim-url-state.md

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ paths:
33
- "apps/sim/app/**/*.tsx"
44
- "apps/sim/app/**/*.ts"
55
- "apps/sim/app/**/search-params.ts"
6+
- "apps/sim/ee/**/*.tsx"
7+
- "apps/sim/ee/**/*.ts"
68
---
79

810
# URL / Query-Param State (nuqs)
@@ -50,11 +52,13 @@ Co-locate a `search-params.ts` next to the feature. Export the parser map (and s
5052

5153
Conventions:
5254

53-
- `.withDefault(...)` on every parser so reads are non-null.
54-
- Filter / search / toggle / pagination options: `{ history: 'replace', shallow: true, clearOnDefault: true }` — clean URLs, no back-stack churn.
55+
- `.withDefault(...)` on every parser so reads are non-null. A deliberately **nullable** parser (dynamic default, custom-range-only dates, nullable sort) must carry a comment saying why.
56+
- Filter / search / toggle / pagination options: `{ history: 'replace', clearOnDefault: true }` — clean URLs, no back-stack churn. Note all three of `history: 'replace'`, `clearOnDefault: true`, and `shallow: true` are already the nuqs v2 defaults — writing the first two explicitly is documentation (and guards the groups whose options differ, e.g. `history: 'push'`), and `shallow: true` may be omitted entirely.
5557
- Navigations that belong in browser history (changing folder, opening a deep-linked entity): `{ history: 'push' }`.
56-
- `shallow: false` **only** when a Server Component / loader must re-read the param.
57-
- Short, stable, **kebab-case** URL keys. Renaming a key is a breaking change to shared links — treat it as one.
58+
- `shallow: false` **only** when a Server Component / loader must re-read the param. For loading states during the server re-render, pass React's `startTransition` via `.withOptions({ startTransition, shallow: false })`.
59+
- Short, stable, **kebab-case** URL keys. Renaming a key is a breaking change to shared links — treat it as one. When the parser-map key is camelCase (for clean destructuring), remap the wire key via the `urlKeys` option in the shared options object (see `files/search-params.ts` `uploadedBy: 'uploaded-by'`, `ee/audit-logs/search-params.ts` `timeRange: 'time-range'`); nuqs also exports a `UrlKeys<typeof parsers>` type helper for standalone mappings.
60+
- `throttleMs` is deprecated in nuqs — rate-limit URL writes with `limitUrlUpdates: throttle(ms)` / `debounce(ms)` (the debounced-search hook below already does this).
61+
- A parser **shared across surfaces with different defaults** (e.g. `parseAsTimeRange`) must `parse` unknown tokens to `null` — never to one surface's default — so each consumer's `.withDefault(...)` decides the fallback.
5862
- For an opaque/literal value use `parseAsStringLiteral([...] as const)`; for a custom wire format use [`createParser`](https://nuqs.dev/docs/parsers).
5963
- A `createParser` for a value **not** comparable with `===` (arrays, objects, `Date`) **must** define an `eq``clearOnDefault` uses it to detect the default, so without it an empty-array/object default never strips from the URL. Built-in `parseAsArrayOf(...)` already ships its own `eq`; only string/number/boolean custom parsers can omit it. Example (array): `eq: (a, b) => a.length === b.length && a.every((v, i) => v === b[i])`.
6064

@@ -75,11 +79,12 @@ export const thingsParsers = {
7579
/** Clean URLs, no back-stack churn for filter changes. */
7680
export const thingsUrlKeys = {
7781
history: 'replace',
78-
shallow: true,
7982
clearOnDefault: true,
8083
} as const
8184
```
8285

86+
(The `*UrlKeys` suffix is the repo's naming convention for a feature's shared **options** object — which may itself contain a nuqs `urlKeys` key-remapping entry; the two are different things.)
87+
8388
### Client — `useQueryStates` (grouped) / `useQueryState` (single)
8489

8590
```typescript
@@ -182,7 +187,7 @@ Sort params live alongside — not inside — the feature's grouped filter parse
182187

183188
A date-only param (a calendar anchor, a date filter) is stored as `yyyy-MM-dd` — never serialize a full `Date`/timestamp when only the day matters.
184189

185-
**Local vs UTC — pick the parser that matches your date math.** nuqs's built-in `parseAsIsoDate` is **UTC-based** (`serialize` via `toISOString()`, `parse` to UTC midnight). If your `Date` is local-time (e.g. produced by local-time helpers and read by `date-fns` `startOfWeek`/`isSameDay`, which are all local), `parseAsIsoDate` will shift the day by ±1 in any non-UTC timezone on reload/deep-link/back-forward. For local-time date math, use a small local-date `createParser` that serializes/parses on local calendar fields (`getFullYear`/`getMonth`/`getDate``new Date(y, m-1, d)`) with an `eq` comparing y/m/d. Only use `parseAsIsoDate` when the value is genuinely UTC/midnight-UTC. See `scheduled-tasks/search-params.ts` (`parseAsLocalDate`).
190+
**Local vs UTC — pick the parser that matches your date math.** nuqs's built-in `parseAsIsoDate` is **UTC-based** (`serialize` via `toISOString().slice(0, 10)`, `parse` to UTC midnight). If your `Date` is local-time (e.g. produced by local-time helpers and read by `date-fns` `startOfWeek`/`isSameDay`, which are all local), `parseAsIsoDate` will shift the day by ±1 in any non-UTC timezone on reload/deep-link/back-forward. For local-time date math, use a small local-date `createParser` that serializes/parses on local calendar fields (`getFullYear`/`getMonth`/`getDate``new Date(y, m-1, d)`) with an `eq` comparing y/m/d. Only use `parseAsIsoDate` when the value is genuinely UTC/midnight-UTC. See `scheduled-tasks/search-params.ts` (`parseAsLocalDate`).
186191

187192
When the default is **dynamic** (e.g. "today"), make the param **nullable** (omit `.withDefault`) and derive the fallback in the hook (`const anchor = param ?? today`), so a clean URL means the dynamic default and navigating back to it writes `null` (clears the param). See `scheduled-tasks/hooks/use-calendar.ts`.
188193

@@ -200,7 +205,11 @@ const [skillId, setSkillId] = useQueryState(skillIdParam.key, {
200205
const editingSkill = skillId ? (skills.find((s) => s.id === skillId) ?? null) : null
201206
```
202207

203-
Open the panel/modal when the id resolves to a loaded entity; closing it calls `setSkillId(null)`. Because this reads `useSearchParams` it needs a **Suspense** boundary on the page (see below). A separate "create new" flow has no id and stays in local `useState`.
208+
Open the panel/modal only when the id **resolves to a loaded entity** — never gate on the raw param alone, or a dead/stale id (deleted entity, old bookmark) renders a broken detail view and a still-loading list flashes one. A dead id simply falls back to the list; the lingering param is harmless. Because this reads `useSearchParams` it needs a **Suspense** boundary on the page (see "Suspense boundary" above). A separate "create new" flow has no id and stays in local `useState`.
209+
210+
**Close with `replace`, open with `push`.** Opening pushed a history entry; closing must not push another. Close via the setter's per-call options — `setSkillId(null, { history: 'replace' })` — so Back from the list leaves the page instead of reopening the detail (see `mcp.tsx`, `workflow-mcp-servers.tsx`, access-control, custom-blocks, forks). Secondary params scoped to the detail view (e.g. its active tab, `server-tab`) are cleared in the same close handler with their own setter — nuqs batches same-tick writes into one URL update.
211+
212+
**Reusable components** rendered both as a settings/list page and inside a modal (e.g. `BYOKKeyManager`) expose an optional controlled `searchTerm`/`onSearchTermChange` prop pair: the page consumer binds the URL (`useSettingsSearch()`), modal consumers omit the props and keep local state. Never bind URL state from inside a component that can mount in a non-destination context.
204213

205214
## Read-then-strip deep links
206215

@@ -217,8 +226,8 @@ The workflow editor (`apps/sim/app/workspace/[workspaceId]/w/**`) is realtime/so
217226

218227
Borderline candidates that *look* shareable but currently stay in Zustand because moving them fights existing machinery:
219228

220-
- **Panel `activeTab`** and **`canvasMode`**persisted local *preferences* wired into an SSR flash-prevention path (`data-panel-active-tab` + `_hasHydrated`). They are layout prefs, not destinations; moving them would unwind the SSR machinery and risk tab-flash on load.
221-
- **`focusedBlockId`** ("look at this block") — the only genuinely shareable candidate, but it is entangled with the persisted editor store and panel-open orchestration. Adding it is a *new feature*, not a migration; ship it deliberately (with runtime verification against a live socket), not as part of a sweep.
229+
- **Panel `activeTab`** — a persisted local *preference* wired into an SSR flash-prevention path (`data-panel-active-tab` + `_hasHydrated`); moving it would unwind that machinery and risk tab-flash on load. **Canvas mode** (`mode` on `useCanvasModeStore`) is likewise a persisted layout preference, not a destination.
230+
- **The panel editor's `currentBlockId`** (`stores/panel/editor/store.ts` — a would-be "look at this block" deep link) — the only genuinely shareable candidate, but it is persisted and entangled with panel-open orchestration. Adding a URL param for it is a *new feature*, not a migration; ship it deliberately (with runtime verification against a live socket), not as part of a sweep.
222231

223232
Rule of thumb for the editor: if state is socket-coupled, high-frequency, viewport-related, or a persisted resize/preference, it stays in Zustand. When in doubt, leave it and flag it — do not force fragile URL state into the canvas.
224233

.cursor/commands/add-block.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,25 @@ export const {ServiceName}Block: BlockConfig = {
138138

139139
**Scope descriptions:** When adding a new OAuth provider, also add human-readable descriptions for all scopes in `SCOPE_DESCRIPTIONS` within `lib/oauth/utils.ts`.
140140

141+
**Service accounts (shared, app-level credentials):** A plain `oauth-input` already lets users *select* an existing service account — those credentials fold into the picker automatically (a Google service account created for any Google service appears in every Google block's picker). You only set `credentialKind` when you want to change the *connect* action:
142+
143+
```typescript
144+
{
145+
id: 'credential',
146+
title: 'Account',
147+
type: 'oauth-input',
148+
serviceId: '{service}',
149+
requiredScopes: getScopesForService('{service}'),
150+
credentialKind: 'any', // omit | 'service-account' | 'any'
151+
}
152+
```
153+
154+
- **omit (default):** lists OAuth accounts + any existing service accounts; the only connect action is "Connect account" (OAuth). Use this for the common "let users pick a service account someone set up elsewhere, but don't offer inline setup" case — no config needed.
155+
- **`'service-account'`:** service-account credentials *only*, plus an inline setup action that opens the provider's connect modal. Use when a block accepts *only* an app credential.
156+
- **`'any'`:** merged picker — OAuth accounts *and* service accounts in one grouped dropdown, with a connect action for each. Use when a block supports both (e.g. Slack: a personal account *or* a custom bot).
157+
158+
Optional companions: `credentialLabels` (override the picker's section/connect-row copy) and `allowServiceAccounts: true` (trigger-mode only — list service accounts, which triggers otherwise exclude; set only when the trigger's polling path can resolve a service-account token). The connect modal, provider families (Google JSON key, Atlassian token, token-paste, client-credential, Slack bot), and the preview gate are all resolved from `serviceAccountProviderId` — you don't wire them per block.
159+
141160
### Selectors (with dynamic options)
142161
```typescript
143162
// Channel selector (Slack, Discord, etc.)

.github/workflows/ci.yml

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,11 @@ jobs:
3636
secrets: inherit
3737

3838
# Detect if this is a version release commit (e.g., "v0.5.24: ...")
39+
# Smallest runner on purpose: a few seconds of pure shell over the commit
40+
# message, no checkout and no install.
3941
detect-version:
4042
name: Detect Version
41-
runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }}
43+
runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-2vcpu-ubuntu-2404' || 'ubuntu-latest' }}
4244
timeout-minutes: 5
4345
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging' || github.ref == 'refs/heads/dev')
4446
outputs:
@@ -69,7 +71,13 @@ jobs:
6971
migrate:
7072
name: Migrate DB
7173
needs: [test-build]
74+
# Explicit need results instead of the implicit success(): a skipped job
75+
# anywhere in the transitive needs chain silently fails implicit success()
76+
# and cascade-skips the deploy chain (migrate -> promote-images ->
77+
# CodeDeploy) — this bit us on 2026-07-23. State requirements explicitly.
7278
if: >-
79+
!cancelled() &&
80+
needs.test-build.result == 'success' &&
7381
github.event_name == 'push' &&
7482
(github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging')
7583
uses: ./.github/workflows/migrations.yml
@@ -297,7 +305,11 @@ jobs:
297305
promote-images:
298306
name: Promote Images
299307
needs: [migrate, build-amd64]
308+
# Explicit results: see migrate's comment.
300309
if: >-
310+
!cancelled() &&
311+
needs.migrate.result == 'success' &&
312+
needs.build-amd64.result == 'success' &&
301313
github.event_name == 'push' &&
302314
(github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging')
303315
runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-2vcpu-ubuntu-2404' || 'ubuntu-latest' }}
@@ -420,7 +432,13 @@ jobs:
420432
runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-2vcpu-ubuntu-2404' || 'ubuntu-latest' }}
421433
timeout-minutes: 10
422434
needs: [promote-images, build-ghcr-arm64, detect-version]
423-
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
435+
# Explicit results: see migrate's comment.
436+
if: >-
437+
!cancelled() &&
438+
needs.promote-images.result == 'success' &&
439+
needs.build-ghcr-arm64.result == 'success' &&
440+
needs.detect-version.result == 'success' &&
441+
github.event_name == 'push' && github.ref == 'refs/heads/main'
424442
permissions:
425443
contents: read
426444
packages: write
@@ -481,9 +499,11 @@ jobs:
481499
fi
482500
483501
# Check if docs changed
502+
# Smallest runner on purpose: a depth-2 checkout plus a path filter, no
503+
# install and no build.
484504
check-docs-changes:
485505
name: Check Docs Changes
486-
runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }}
506+
runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-2vcpu-ubuntu-2404' || 'ubuntu-latest' }}
487507
timeout-minutes: 5
488508
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
489509
outputs:
@@ -505,7 +525,12 @@ jobs:
505525
process-docs:
506526
name: Process Docs
507527
needs: [promote-images, check-docs-changes]
508-
if: needs.check-docs-changes.outputs.docs_changed == 'true'
528+
# Explicit results: see migrate's comment.
529+
if: >-
530+
!cancelled() &&
531+
needs.promote-images.result == 'success' &&
532+
needs.check-docs-changes.result == 'success' &&
533+
needs.check-docs-changes.outputs.docs_changed == 'true'
509534
uses: ./.github/workflows/docs-embeddings.yml
510535
secrets: inherit
511536

@@ -515,7 +540,12 @@ jobs:
515540
runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }}
516541
timeout-minutes: 10
517542
needs: [create-ghcr-manifests, detect-version]
518-
if: needs.detect-version.outputs.is_release == 'true'
543+
# Explicit results: see migrate's comment.
544+
if: >-
545+
!cancelled() &&
546+
needs.create-ghcr-manifests.result == 'success' &&
547+
needs.detect-version.result == 'success' &&
548+
needs.detect-version.outputs.is_release == 'true'
519549
permissions:
520550
contents: write
521551
steps:

0 commit comments

Comments
 (0)