diff --git a/.agents/skills/add-enrichment/SKILL.md b/.agents/skills/add-enrichment/SKILL.md index 35ef4596b24..8ac7f58a95c 100644 --- a/.agents/skills/add-enrichment/SKILL.md +++ b/.agents/skills/add-enrichment/SKILL.md @@ -22,8 +22,8 @@ Because enrichments run on Sim's hosted keys by default, **every provider tool y ## Architecture (what you're plugging into) -- **`enrichments/types.ts`** — `EnrichmentConfig { id, name, description, icon, inputs, outputs, providers }` and `EnrichmentProvider { id, label, toolId, buildParams, projectFailure, mapOutput, isVerified? }` — `toolProvider` fills `projectFailure` with the standard HTTP projection; override it only for a provider whose failure shape is nonstandard (see `enrichments/provider-failures/`). Providers are **plain data** (no `@/tools` import) so the catalog stays client-safe. -- **`enrichments/providers.ts`** — `toolProvider(...)` (typed passthrough) plus shared input helpers: `str(v)`, `normalizeDomain(v)`, `firstNonEmpty(arr)`, `splitName(fullName)`, and `projectEnrichmentProviderFailure`. +- **`enrichments/types.ts`** — `EnrichmentConfig { id, name, description, icon, inputs, outputs, providers }` and `EnrichmentProvider { id, label, toolId, buildParams, projectFailure, mapOutput, isVerified? }` — `toolProvider` fills `projectFailure` with the standard HTTP projection; override it only for a provider whose failure shape is nonstandard. Providers are **plain data** (no `@/tools` import) so the catalog stays client-safe. +- **`enrichments/providers.ts`** — `toolProvider(...)` (typed passthrough) plus shared input helpers: `str(v)`, `normalizeDomain(v)`, `splitName(fullName)`, and `projectEnrichmentProviderFailure`. - **`enrichments/run.ts`** — the server-only cascade runner. Calls `executeTool(provider.toolId, { ...params, _context: { workspaceId, userId } })`, accumulates hosted-key cost, returns the first non-empty mapped result. **You do not edit this** — it works for any registry entry. - **`enrichments/registry.ts`** — `ENRICHMENT_REGISTRY` / `ALL_ENRICHMENTS` / `getEnrichment`. Register new entries here. @@ -111,7 +111,7 @@ export { myEnrichment } from './my-enrichment' ``` Rules: -- Keep the file **client-safe**: import only `@sim/emcn/icons`, `@sim/utils/*`, `@/enrichments/providers`, `@/enrichments/provider-failures/*`, and the types. **Never import `@/tools`** here — the runner does the tool call. +- Keep the file **client-safe**: import only `@sim/emcn/icons`, `@sim/utils/*`, `@/enrichments/providers`, and the types. **Never import `@/tools`** here — the runner does the tool call. - `buildParams` returns `null` when inputs are insufficient (provider skipped). `mapOutput` returns `null`/empty for a miss (falls through). Use `filterUndefined` when assembling optional tool params; coerce numbers explicitly (don't pass `''` to number outputs). - Output `id`s are the keys `mapOutput` returns; output `name`s are the default column names (the user can rename them in the config). diff --git a/.agents/skills/add-trigger/SKILL.md b/.agents/skills/add-trigger/SKILL.md index 53311498b56..33473112aa0 100644 --- a/.agents/skills/add-trigger/SKILL.md +++ b/.agents/skills/add-trigger/SKILL.md @@ -411,7 +411,7 @@ export const {service}PollingHandler: PollingProviderHandler = { await markWebhookSuccess(webhookId, logger) return 'success' } catch (error) { - logger.error(`[${requestId}] Error processing {service} webhook ${webhookId}:`, error) + logger.error('Error processing {service} webhook:', error) await markWebhookFailed(webhookId, logger) return 'failure' } diff --git a/.claude/rules/sim-platform-views.md b/.claude/rules/sim-platform-views.md new file mode 100644 index 00000000000..f72b0e18551 --- /dev/null +++ b/.claude/rules/sim-platform-views.md @@ -0,0 +1,65 @@ +--- +description: One pattern per job across the organization views (Settings, People, Integrations, Links, Home) +paths: + - "apps/sim/platform/app/(shell)/{settings,people,integrations,links,home}/**" + - "apps/sim/platform/design-system/components/{status,modal,picker,settings-section,search-field,activity-row}/**" + - "apps/sim/app/o/*/**" +--- + +# Organization Views + +The organization views live in `apps/sim/platform/app/(shell)/` and take everything shared from +`@/platform/design-system/components`. Each job below has one pattern. A view that differs needs a +reason written where it differs; "it was already like that" is not one. + +| Job | Use | Never | +| --- | --- | --- | +| First read of a view | `QueryBoundary` (`query`, `failure`, `loadingLabel`, optional `empty`, `loading`), which renders `LoadingRows`, then `FailureNotice`, then `empty`, then `children(data)` | Inline `isPending ? … : error ? …`, a raw `

Loading…

`, a loading `InlineNotice` | +| Loading | `LoadingRows` by default. A placeholder shaped like its content (`WidgetSkeleton`, usage charts) only where rows would misrepresent it | A second generic skeleton | +| Failed read | `FailureNotice`, labelled Retry. `intent='warning'` where the read partly answered | A hand-built retry button or `InlineNotice` + `Button` | +| Failed background refresh | `FailureNotice` outside the boundary, over the content it keeps | Replacing content already on screen | +| Empty | `EmptyState`, with no create button when the page already has one. A picker's no-match line inside a dialog stays `Muted`, since a block would take over the dialog | `Muted` or a raw `

` anywhere else | +| Confirm | `ConfirmDialog` (`target`, `title`, `confirmLabel`, `pendingLabel`, `onConfirm`, `safe` when nothing is lost). Removing a person, revoking an invite and any destructive row action always ask first | `Modal variant='confirm'`, `window.confirm`, an unconfirmed destructive action | +| Save | `SaveActions`, end-aligned in the page flow under the fields it commits: Discard while dirty, then Save. Save is visible and disabled while clean | Save in a header, Save before Discard, a Cancel beside Discard | +| Editing a server value | `useServerDraft`. Adopts a newer value while clean; while dirty it keeps the edits and sets `conflict`. Show `DraftConflictNotice`, and block Save until the viewer chooses | Seeding `useState` from a query once | +| Autosave vs Save | One or the other per page. A page with `SaveActions` holds every field in one draft and writes it in one request. List actions with their own dialog, such as adding or removing a member, are not field edits | A page mixing both | +| Create | A primary compact button in `SectionToolbar`, or in the page header on a page with one | Create in both the toolbar and the empty state | +| Search over a section's rows | `SectionSearch subject='members'` | `SearchField` with its own copy, unless the search covers more than the subject's name | +| Row | `ActivityRow` for a row that opens something, `SettingsRow` for a row holding a control | A hand-built row | +| List of rows | `RowList` (`dividers` for a list read as one block). `ActivityRow density='comfortable'` in settings | `calc(-1 * var(--ui-row-inset))` around a list; styling a row's height from outside | +| Row actions | `ActionMenu` with icon `I108`, destructive item last with a separator | The same action in the row and its menu | +| Section heading | `SettingsSection title` (h2), `DetailHeader` for a sub-view's way back | A hand-written heading | +| Toolbar | `SectionToolbar grow={}` with its actions as children | A hand-built flex row | + +## Failure feedback + +- Form validation is inline, on the field (`error`), once a save has been attempted. +- A dialog's own action failing stays in the dialog: `Modal` shows a rejected `onConfirm`'s message and keeps the draft. +- Any other action failing is a toast. Nothing fails silently. + +## Page gates + +Every page under `app/o/[organizationId]` calls +`requireOrganizationViewer(organizationId, callbackUrl)` from `@/app/o/[organizationId]/viewer`. It +redirects a signed-out viewer to sign-in and back, 404s a viewer with no access, and returns +`{ userId, principal, surface }`. Never write `{ kind: 'session', ... }` in a page. + +## Dialogs + +`Modal` holds itself open while `onConfirm`'s promise or its `pending` prop runs. Escape, the backdrop +and Close do nothing until it settles, including when the browser closes the `

` itself: once +user activation is spent, `cancel` cannot be prevented, so `OverlayDialog` reopens from `close`. + +## Files + +- A section with more than one job splits by job: `group-detail.tsx` is the shell, `group-general-tab.tsx` + and `group-catalog-tabs.tsx` its bodies, `use-group-draft.ts` its write. +- Merge logic worth a test is a pure module next to the view, such as `secrets-draft.ts`. +- `section-parts/` holds what only settings needs: `SectionStack`, `FieldStack`, `SectionToolbar`, + `DetailHeader`, `HeadingRow`, `Muted`, `SectionSkeleton`, `DraftConflictNotice`. `useServerDraft` is in `_components/`, since Integrations uses it too. + +## Legacy workspace settings + +`apps/sim/app/workspace/*/settings/**` keeps `SettingsPanel` from `@/components/settings/settings-panel`, +which owns chrome through `settings/[section]/layout.tsx`. The `add-settings-page` skill describes it. +New organization work never imports it. diff --git a/.claude/rules/sim-settings-pages.md b/.claude/rules/sim-settings-pages.md index c39babca114..d10630c69b9 100644 --- a/.claude/rules/sim-settings-pages.md +++ b/.claude/rules/sim-settings-pages.md @@ -2,7 +2,6 @@ description: Settings page layout and SettingsPanel design-system compliance paths: - "apps/sim/app/workspace/*/settings/**" - - "apps/sim/app/o/*/settings/**" - "apps/sim/app/workspace/*/{integrations,skills,upgrade}/**" - "apps/sim/app/workspace/*/components/{resource-tile,credential-detail}/**" - "apps/sim/components/{settings,permissions}/**" diff --git a/.cursor/rules/sim-platform-views.mdc b/.cursor/rules/sim-platform-views.mdc new file mode 100644 index 00000000000..9cbcc0383a2 --- /dev/null +++ b/.cursor/rules/sim-platform-views.mdc @@ -0,0 +1,64 @@ +--- +description: "One pattern per job across the organization views (Settings, People, Integrations, Links, Home)" +globs: ["apps/sim/platform/app/(shell)/{settings,people,integrations,links,home}/**","apps/sim/platform/design-system/components/{status,modal,picker,settings-section,search-field,activity-row}/**","apps/sim/app/o/*/**"] +--- + + + +# Organization Views + +The organization views live in `apps/sim/platform/app/(shell)/` and take everything shared from +`@/platform/design-system/components`. Each job below has one pattern. A view that differs needs a +reason written where it differs; "it was already like that" is not one. + +| Job | Use | Never | +| --- | --- | --- | +| First read of a view | `QueryBoundary` (`query`, `failure`, `loadingLabel`, optional `empty`, `loading`), which renders `LoadingRows`, then `FailureNotice`, then `empty`, then `children(data)` | Inline `isPending ? … : error ? …`, a raw `

Loading…

`, a loading `InlineNotice` | +| Loading | `LoadingRows` by default. A placeholder shaped like its content (`WidgetSkeleton`, usage charts) only where rows would misrepresent it | A second generic skeleton | +| Failed read | `FailureNotice`, labelled Retry. `intent='warning'` where the read partly answered | A hand-built retry button or `InlineNotice` + `Button` | +| Failed background refresh | `FailureNotice` outside the boundary, over the content it keeps | Replacing content already on screen | +| Empty | `EmptyState`, with no create button when the page already has one. A picker's no-match line inside a dialog stays `Muted`, since a block would take over the dialog | `Muted` or a raw `

` anywhere else | +| Confirm | `ConfirmDialog` (`target`, `title`, `confirmLabel`, `pendingLabel`, `onConfirm`, `safe` when nothing is lost). Removing a person, revoking an invite and any destructive row action always ask first | `Modal variant='confirm'`, `window.confirm`, an unconfirmed destructive action | +| Save | `SaveActions`, end-aligned in the page flow under the fields it commits: Discard while dirty, then Save. Save is visible and disabled while clean | Save in a header, Save before Discard, a Cancel beside Discard | +| Editing a server value | `useServerDraft`. Adopts a newer value while clean; while dirty it keeps the edits and sets `conflict`. Show `DraftConflictNotice`, and block Save until the viewer chooses | Seeding `useState` from a query once | +| Autosave vs Save | One or the other per page. A page with `SaveActions` holds every field in one draft and writes it in one request. List actions with their own dialog, such as adding or removing a member, are not field edits | A page mixing both | +| Create | A primary compact button in `SectionToolbar`, or in the page header on a page with one | Create in both the toolbar and the empty state | +| Search over a section's rows | `SectionSearch subject='members'` | `SearchField` with its own copy, unless the search covers more than the subject's name | +| Row | `ActivityRow` for a row that opens something, `SettingsRow` for a row holding a control | A hand-built row | +| List of rows | `RowList` (`dividers` for a list read as one block). `ActivityRow density='comfortable'` in settings | `calc(-1 * var(--ui-row-inset))` around a list; styling a row's height from outside | +| Row actions | `ActionMenu` with icon `I108`, destructive item last with a separator | The same action in the row and its menu | +| Section heading | `SettingsSection title` (h2), `DetailHeader` for a sub-view's way back | A hand-written heading | +| Toolbar | `SectionToolbar grow={}` with its actions as children | A hand-built flex row | + +## Failure feedback + +- Form validation is inline, on the field (`error`), once a save has been attempted. +- A dialog's own action failing stays in the dialog: `Modal` shows a rejected `onConfirm`'s message and keeps the draft. +- Any other action failing is a toast. Nothing fails silently. + +## Page gates + +Every page under `app/o/[organizationId]` calls +`requireOrganizationViewer(organizationId, callbackUrl)` from `@/app/o/[organizationId]/viewer`. It +redirects a signed-out viewer to sign-in and back, 404s a viewer with no access, and returns +`{ userId, principal, surface }`. Never write `{ kind: 'session', ... }` in a page. + +## Dialogs + +`Modal` holds itself open while `onConfirm`'s promise or its `pending` prop runs. Escape, the backdrop +and Close do nothing until it settles, including when the browser closes the `

` itself: once +user activation is spent, `cancel` cannot be prevented, so `OverlayDialog` reopens from `close`. + +## Files + +- A section with more than one job splits by job: `group-detail.tsx` is the shell, `group-general-tab.tsx` + and `group-catalog-tabs.tsx` its bodies, `use-group-draft.ts` its write. +- Merge logic worth a test is a pure module next to the view, such as `secrets-draft.ts`. +- `section-parts/` holds what only settings needs: `SectionStack`, `FieldStack`, `SectionToolbar`, + `DetailHeader`, `HeadingRow`, `Muted`, `SectionSkeleton`, `DraftConflictNotice`. `useServerDraft` is in `_components/`, since Integrations uses it too. + +## Legacy workspace settings + +`apps/sim/app/workspace/*/settings/**` keeps `SettingsPanel` from `@/components/settings/settings-panel`, +which owns chrome through `settings/[section]/layout.tsx`. The `add-settings-page` skill describes it. +New organization work never imports it. diff --git a/.cursor/rules/sim-settings-pages.mdc b/.cursor/rules/sim-settings-pages.mdc index aeb87b3325a..59a148afae9 100644 --- a/.cursor/rules/sim-settings-pages.mdc +++ b/.cursor/rules/sim-settings-pages.mdc @@ -1,6 +1,6 @@ --- description: "Settings page layout and SettingsPanel design-system compliance" -globs: ["apps/sim/app/workspace/*/settings/**","apps/sim/app/o/*/settings/**","apps/sim/app/workspace/*/{integrations,skills,upgrade}/**","apps/sim/app/workspace/*/components/{resource-tile,credential-detail}/**","apps/sim/components/{settings,permissions}/**","apps/sim/ee/**/components/**"] +globs: ["apps/sim/app/workspace/*/settings/**","apps/sim/app/workspace/*/{integrations,skills,upgrade}/**","apps/sim/app/workspace/*/components/{resource-tile,credential-detail}/**","apps/sim/components/{settings,permissions}/**","apps/sim/ee/**/components/**"] --- diff --git a/.github/ci-suites.json b/.github/ci-suites.json index 48b3bf98c08..fbd721966a9 100644 --- a/.github/ci-suites.json +++ b/.github/ci-suites.json @@ -24,6 +24,10 @@ "group": "organization-search", "task": "ci:e2e-organization-search" }, + { + "group": "org-redesign", + "task": "ci:e2e-org-redesign" + }, { "group": "browser-extension-navigation", "task": "ci:e2e-browser-extension" diff --git a/.github/scripts/ci-plan.gql b/.github/scripts/ci-plan.gql index 78dcbf3eaab..5f44d548823 100644 --- a/.github/scripts/ci-plan.gql +++ b/.github/scripts/ci-plan.gql @@ -3,7 +3,7 @@ query($base: String!, $head: String!) { affectedTasks(base: $base, head: $head, tasks: [ "test", "build", "type-check", "ci:integration", "ci:desktop-live", "ci:e2e-scim", "ci:e2e-cli", "ci:e2e-stop-after", "ci:e2e-desktop-inbox", - "ci:e2e-assistant-actions", "ci:e2e-organization-search", + "ci:e2e-assistant-actions", "ci:e2e-organization-search", "ci:e2e-org-redesign", "ci:e2e-docs-platform", "ci:e2e-browser-extension", "ci:e2e-project-files", "ci:e2e-mobile", "ci:e2e-seo" ]) { diff --git a/.github/scripts/http-e2e.sh b/.github/scripts/http-e2e.sh index 8c5f185e7cd..f3e43d7cadb 100755 --- a/.github/scripts/http-e2e.sh +++ b/.github/scripts/http-e2e.sh @@ -3,7 +3,7 @@ # CI groups, which reuse the production build from `e2e-build`. # The job extracts it into apps/sim/.next before running these suites. # -# Usage: http-e2e.sh (run from apps/sim) +# Usage: http-e2e.sh (run from apps/sim) # # The job provides DATABASE_URL, BETTER_AUTH_SECRET and ENCRYPTION_KEY; each group sets the rest of # its app's environment here. Reports and server logs land in $RUNNER_TEMP/e2e. @@ -20,7 +20,7 @@ # telemetry flush runs detached and still writes .next/dev). set -euo pipefail -group=${1:?usage: http-e2e.sh } +group=${1:?usage: http-e2e.sh } report_dir="$RUNNER_TEMP/e2e" ready_timeout_seconds=600 mkdir -p "$report_dir" @@ -32,6 +32,7 @@ status_log='' realtime_pid='' realtime_tag='' realtime_log='' +agent_pid='' browsers_pid='' local_app_url='' # `dev` or `start`; a group sets it before start_app. @@ -63,6 +64,10 @@ finish() { tail -n 100 "$realtime_log" fi fi + if [ -n "$agent_pid" ]; then + kill "$agent_pid" 2>/dev/null || true + wait "$agent_pid" 2>/dev/null || true + fi if [ -n "$browsers_pid" ]; then kill -- "-$browsers_pid" 2>/dev/null || true wait "$browsers_pid" 2>/dev/null || true @@ -133,6 +138,13 @@ start_realtime() { wait_ready "$realtime_pid" "http://127.0.0.1:$port/health" "$name realtime server" } +# stop_app: stops the app start_app started, so a group can start it again under other flags. +stop_app() { + bash "$GITHUB_WORKSPACE/.github/scripts/stop-session.sh" "$server_pid" "$app_tag" + wait "$server_pid" 2>/dev/null || true + server_pid='' +} + # wait_ready