Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
fd0d08a
improvement(mcp): push-driven settings freshness + lifecycle-audit fi…
waleedlatif1 Jul 22, 2026
a0a437d
fix(deps): bump sharp to 0.35.3 and js-yaml to 4.3.0 for security adv…
waleedlatif1 Jul 22, 2026
e841e4e
fix(mcp): cap transport response bodies to bound a hostile tools/call…
waleedlatif1 Jul 22, 2026
70814bc
feat(db): role-keyed dbFor clients for cleanup and exec workloads (#5…
TheodoreSpeaks Jul 22, 2026
936536e
improvement(url-state): platform-wide nuqs audit — migrate remaining …
waleedlatif1 Jul 22, 2026
fad9728
improvement(tests): make vitest suites state-safe with auto-unstub of…
waleedlatif1 Jul 22, 2026
b49fe16
improvement(logs): show Redacting status while PII masking runs (#5855)
TheodoreSpeaks Jul 22, 2026
d7d42fa
fix(mothership): stop-button transitions freeze in place (#5838)
TheodoreSpeaks Jul 22, 2026
5338485
feat(sidebar): add Slack Community link to help dropdown (#5858)
waleedlatif1 Jul 22, 2026
6f33a94
feat(workflows): IDE-style reference viewer for workflows (#5854)
waleedlatif1 Jul 22, 2026
ae7b5ef
feat(copilot): service account setup & reconnect in chat (#5786)
TheodoreSpeaks Jul 22, 2026
3215a12
improvement(testing): consolidate @sim/db mocks into one table-aware …
waleedlatif1 Jul 22, 2026
49d3804
fix(ci): save the Next.js build cache every run instead of freezing i…
waleedlatif1 Jul 22, 2026
c083be9
improvement(auth): bump better-auth to 1.6.23 and add trusted-proxy c…
waleedlatif1 Jul 22, 2026
62a8ce4
improvement(tests): db-mock migration tranche 1 — knowledge, billing/…
waleedlatif1 Jul 22, 2026
52659d4
improvement(tests): db-mock migration tranche 2 — copilot, mothership…
waleedlatif1 Jul 22, 2026
51307c4
improvement(tests): db-mock migration tranche 3 — lib/workflows, lib/…
waleedlatif1 Jul 22, 2026
21ac7b1
improvement(tests): db-mock migration tranche 4 — billing, webhooks/e…
waleedlatif1 Jul 22, 2026
8cce661
feat(api): proxyUrl for residential/custom proxy egress on the API bl…
waleedlatif1 Jul 22, 2026
2b5a92a
feat(auth): org session policies — lifetime/idle limits, org-wide rev…
waleedlatif1 Jul 22, 2026
02bc8f1
feat(ci): warm Next.js builds via Turbopack persistent cache on a sti…
waleedlatif1 Jul 23, 2026
9d8e14c
improvement(tests): converge env-flags mocks onto a complete shared m…
waleedlatif1 Jul 23, 2026
dda602a
improvement(tests+ci): phase 3 — shared-mock convergence completion a…
waleedlatif1 Jul 23, 2026
227cd65
fix(mcp): bound and retry OAuth start so a transient stall recovers i…
waleedlatif1 Jul 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
19 changes: 19 additions & 0 deletions .agents/skills/add-block/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,25 @@ export const {ServiceName}Block: BlockConfig = {

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

**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:

```typescript
{
id: 'credential',
title: 'Account',
type: 'oauth-input',
serviceId: '{service}',
requiredScopes: getScopesForService('{service}'),
credentialKind: 'any', // omit | 'service-account' | 'any'
}
```

- **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.
- **`'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.
- **`'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).

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.

### Selectors (with dynamic options)
```typescript
// Channel selector (Slack, Discord, etc.)
Expand Down
19 changes: 19 additions & 0 deletions .claude/commands/add-block.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,25 @@ export const {ServiceName}Block: BlockConfig = {

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

**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:

```typescript
{
id: 'credential',
title: 'Account',
type: 'oauth-input',
serviceId: '{service}',
requiredScopes: getScopesForService('{service}'),
credentialKind: 'any', // omit | 'service-account' | 'any'
}
```

- **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.
- **`'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.
- **`'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).

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.

### Selectors (with dynamic options)
```typescript
// Channel selector (Slack, Discord, etc.)
Expand Down
27 changes: 18 additions & 9 deletions .claude/rules/sim-url-state.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ paths:
- "apps/sim/app/**/*.tsx"
- "apps/sim/app/**/*.ts"
- "apps/sim/app/**/search-params.ts"
- "apps/sim/ee/**/*.tsx"
- "apps/sim/ee/**/*.ts"
---

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

Conventions:

- `.withDefault(...)` on every parser so reads are non-null.
- Filter / search / toggle / pagination options: `{ history: 'replace', shallow: true, clearOnDefault: true }` — clean URLs, no back-stack churn.
- `.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.
- 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.
- Navigations that belong in browser history (changing folder, opening a deep-linked entity): `{ history: 'push' }`.
- `shallow: false` **only** when a Server Component / loader must re-read the param.
- Short, stable, **kebab-case** URL keys. Renaming a key is a breaking change to shared links — treat it as one.
- `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 })`.
- 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.
- `throttleMs` is deprecated in nuqs — rate-limit URL writes with `limitUrlUpdates: throttle(ms)` / `debounce(ms)` (the debounced-search hook below already does this).
- 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.
- For an opaque/literal value use `parseAsStringLiteral([...] as const)`; for a custom wire format use [`createParser`](https://nuqs.dev/docs/parsers).
- 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])`.

Expand All @@ -75,11 +79,12 @@ export const thingsParsers = {
/** Clean URLs, no back-stack churn for filter changes. */
export const thingsUrlKeys = {
history: 'replace',
shallow: true,
clearOnDefault: true,
} as const
```

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

### Client — `useQueryStates` (grouped) / `useQueryState` (single)

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

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.

**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`).
**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`).

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

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

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

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

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

## Read-then-strip deep links

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

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

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

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.

Expand Down
19 changes: 19 additions & 0 deletions .cursor/commands/add-block.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,25 @@ export const {ServiceName}Block: BlockConfig = {

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

**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:

```typescript
{
id: 'credential',
title: 'Account',
type: 'oauth-input',
serviceId: '{service}',
requiredScopes: getScopesForService('{service}'),
credentialKind: 'any', // omit | 'service-account' | 'any'
}
```

- **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.
- **`'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.
- **`'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).

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.

### Selectors (with dynamic options)
```typescript
// Channel selector (Slack, Discord, etc.)
Expand Down
Loading
Loading