diff --git a/.agents/skills/add-settings-page/SKILL.md b/.agents/skills/add-settings-page/SKILL.md index 94ff0f182e7..794d52d3e02 100644 --- a/.agents/skills/add-settings-page/SKILL.md +++ b/.agents/skills/add-settings-page/SKILL.md @@ -1,10 +1,12 @@ --- name: add-settings-page -description: Add a new Sim settings page, or audit existing settings pages for design-system compliance with the shared SettingsPanel layout. Use when creating a settings tab, or when asked to check/clean up settings pages so they match the design system (consistent title, header, search, spacing). +description: Add or audit settings pages that use SettingsPanel in the existing Sim app. Use implement-platform for isolated Platform settings. --- # Settings Page (add / audit) +For `apps/sim/platform/**`, use [implement-platform](../implement-platform/SKILL.md). In a mixed request, apply the steps below only to the existing app's settings. + Settings page chrome (header bar, scroll region, content column, nav-driven title + description) is owned by the `settings/[section]/layout.tsx` shell. Each page renders through **`SettingsPanel`**, which registers the page's header data @@ -45,7 +47,7 @@ For each page component, confirm the checklist in `.claude/rules/sim-settings-pa Each grep lists candidates; review every match against the expected ones named below. 1. Find hand-rolled shells that should be `SettingsPanel`: - `git grep -n "flex h-full flex-col bg-\[var(--bg)\]" -- 'apps/sim/**/settings/**' 'apps/sim/ee/'` + `git grep -n "flex h-full flex-col bg-\[var(--bg)\]" -- 'apps/sim/**/settings/**' 'apps/sim/ee/' ':(exclude)apps/sim/platform/**'` — expected matches: the workspace and organization `settings/layout.tsx` shells and the shared header shell (`components/settings/settings-header.tsx`); an entitlement/loading gate is also fine. `CredentialDetailLayout` (the `settings/secrets/[credentialId]` exception) is @@ -53,12 +55,12 @@ Each grep lists candidates; review every match against the expected ones named b passes `back={{ text, icon: ArrowLeft, onSelect }}` to `SettingsPanel`. Anything else is a violation: render it through `SettingsPanel`. 2. Find hand-rolled title blocks: - `git grep -n "text-\[var(--text-body)\] text-lg" -- 'apps/sim/**/settings/**' 'apps/sim/ee/'` + `git grep -n "text-\[var(--text-body)\] text-lg" -- 'apps/sim/**/settings/**' 'apps/sim/ee/' ':(exclude)apps/sim/platform/**'` — the only title is the `

` in `settings-header.tsx`; a non-heading value at that size (e.g. the credit balance in `ee/organization-usage/components/usage-credits.tsx`) is fine. 3. Find literal pixel text sizes (should be 0 — see "Text Scale" in `.claude/rules/sim-styling.md`): - `git grep -nE "text-\[1[0-8]px\]" -- 'apps/sim/**/settings/**' 'apps/sim/ee/'`. + `git grep -nE "text-\[1[0-8]px\]" -- 'apps/sim/**/settings/**' 'apps/sim/ee/' ':(exclude)apps/sim/platform/**'`. Display type above the scale (`text-[40px]` hero headings) is deliberate and out of scope. 4. Confirm each page imports `SettingsPanel` and that its registry entry has an @@ -66,7 +68,7 @@ Each grep lists candidates; review every match against the expected ones named b - Editable pages: confirm Save/Discard go through `saveDiscardActions()` and dirty is wired via `useSettingsUnsavedGuard` (called before early-return gates) — flag any hand-rolled Save button, `beforeunload`, or unsaved modal. - `git grep -n "beforeunload" -- 'apps/sim/**/settings/**' 'apps/sim/ee/' 'apps/sim/components/settings/' ':(exclude,glob)**/*.test.*'` + `git grep -n "beforeunload" -- 'apps/sim/**/settings/**' 'apps/sim/ee/' 'apps/sim/components/settings/' ':(exclude)apps/sim/platform/**' 'apps/sim/hooks/use-settings-browser-navigation.ts' ':(exclude,glob)**/*.test.*'` should only hit the centralized `use-settings-browser-navigation.ts`. 5. Fix each finding with the smallest structural change that satisfies the checklist; do not touch handlers, state, queries, or gate returns. A pixel-size fix swaps diff --git a/.agents/skills/cleanup/SKILL.md b/.agents/skills/cleanup/SKILL.md index 1ddfddcce6e..af9eb79f1f2 100644 --- a/.agents/skills/cleanup/SKILL.md +++ b/.agents/skills/cleanup/SKILL.md @@ -25,7 +25,7 @@ Run these in parallel on the parsed `scope`: 3. `/you-might-not-need-a-callback fix=false` 4. `/you-might-not-need-state fix=false` 5. `/react-query-best-practices fix=false` -6. `/emcn-design-review fix=false` +6. Design review: route `apps/sim/platform/**` to [implement-platform](../implement-platform/SKILL.md) and existing app EMCN paths to [emcn-design-review](../emcn-design-review/SKILL.md). For a mixed diff, the same pass reviews each subset with its own contract. Both reviews are read-only and return proposals for Step 2. 7. `/you-might-not-need-url-state fix=false` 8. `/you-might-not-need-a-comment fix=false` 9. `/test-audit audit ` — read-only; only when the scope adds or changes test files (`*.test.ts(x)`, `*.integration.ts`, `**/e2e/**`, `apps/sim/scripts/test-*-e2e.ts`). First resolve a free-form scope to the concrete list of added or changed test paths (`git diff --name-only` against the scope's base) and pass those paths. It applies the authoring gate to every new or changed test and proposes deleting the ones that fail it. diff --git a/.agents/skills/emcn-design-review/SKILL.md b/.agents/skills/emcn-design-review/SKILL.md index 8a211f53c6b..8fa95e83a5e 100644 --- a/.agents/skills/emcn-design-review/SKILL.md +++ b/.agents/skills/emcn-design-review/SKILL.md @@ -1,6 +1,6 @@ --- name: emcn-design-review -description: Review UI code for alignment with the emcn design system — components, tokens, patterns, and conventions +description: Review the existing Sim app against packages/emcn. Route isolated Platform UI in apps/sim/platform to implement-platform. argument-hint: "[scope] [fix=true|false]" --- @@ -14,7 +14,7 @@ User arguments: $ARGUMENTS ## Context -This codebase uses **emcn**, a custom component library built on Radix UI primitives with CVA variants and CSS variable design tokens. All UI must use emcn components and tokens. +The existing Sim app uses `packages/emcn`, its Radix primitives, CVA variants, and CSS tokens. For `apps/sim/platform/**`, use [implement-platform](../implement-platform/SKILL.md). For mixed scopes, review each file under its owning system and preserve the requested `fix` mode for both. ## Steps diff --git a/.agents/skills/implement-platform/SKILL.md b/.agents/skills/implement-platform/SKILL.md new file mode 100644 index 00000000000..92833c90da7 --- /dev/null +++ b/.agents/skills/implement-platform/SKILL.md @@ -0,0 +1,17 @@ +--- +name: implement-platform +description: Build, change, consolidate, remove, or review Sim Platform UI in apps/sim/platform. Use for code-first product routes, EMCN components, Paper handoff and design drift in the isolated Next app. +--- + +# Implement Sim Platform + +Start in the Sim checkout containing the requested product work. Read the [Platform contract](../../../apps/sim/platform/AGENTS.md) and [component ownership map](../../../apps/sim/platform/docs/component-ownership.md). These versioned files carry the current rules into a fresh chat. Use the [Platform README](../../../apps/sim/platform/README.md) for the local routes, fixture states, generators, and verification commands. + +1. **Establish scope on the real route.** Identify the product entry, interaction, phase, and affected consumers. When rebuilding an existing workflow, trace the old navigation target, its access gate, and the actions reachable by that viewer. Use that evidence to decide which controls belong on the new page before consulting the broader API or provider catalog. Phase 1 owns foundations and the library. Phase 2 owns working UI and transient interaction state, with examples supplied through the server fixture flag. Phase 3 connects real data and operations. Reuse the same renderer in the browser and Sim Desktop. Read the installed Next guides for the file structure or framework behavior you are changing. + For organization Settings, inventory the old sidebar in order, its viewer/deployment gates and every action reachable from each section. Map old header buttons to the row or section they act on. Verify the same destination and drilldown on the fixture and authenticated routes; keep real writes outside fixture sessions. +2. **Resolve design intent.** For Paper-backed work, use `$sim-paper-design` to locate the live Shared or Platform master and the product states that use it. Exclude Marcus. Preserve the key and theme roles. Treat artboard sizes as reference measurements. For code-first changes, follow the latest explicit product direction and inspect the rendered route. Identify any source you could not verify. Update the Paper canvas when the request includes Paper changes. +3. **Choose or change the owner.** Start with the behavior. An action, navigation row, value picker, or disclosure has an existing keyboard and pointer contract. Different interactions may share visual roles while retaining different semantics. Use the existing family and named props for a change in size, intent, icon, or content. Put unique anatomy and its CSS beside the route component. Keep page files free of styling. Only EMCN owns the appearance of its controls. Use Base UI there for complex keyboard, floating, and dismissal behavior. +4. **Trace the full change.** On a second independent feature use, promote the original implementation and migrate both callers. For consolidation, compare semantics, map every consumer to its replacement, then remove the obsolete implementation, export, variant styles, and unused registrations. For deletion, also check menus, preferences, fixture data, workbench references, and tests that existed only for the removed feature. Preserve unrelated consumers and historical Paper evidence. A new variant needs a reachable product use. Edit the source or compiler for generated foundations and recipes. +5. **Verify where people use it.** Run the design audit and applicable checks from the README. Render the changed product route with empty, full, stress, and fixture-off data as relevant. Check both themes, widths between artboards, long content, text scaling, keyboard, and pointer states. Compare shared controls in their actual consumers. Check the actual scroll edge and the open popup when space is tight. For shell, panel, or desktop work, exercise the real Electron renderer. For substantial implementations and consolidations, use an independent agent during design or implementation and for a final review of the changed owners and routes. Provide the working localhost route for visual review. +6. **Keep non-UI code in one place.** When work touches both the zone and `apps/sim/platform/embedded`, converge first rather than porting: only markup may differ. Follow "Converging a domain" in `apps/sim/platform/AGENTS.md`; `bun run check:platform-convergence` fails a new parallel file. +7. **Keep decisions durable.** Follow the contract’s maintenance guidance when an approved correction changes a shared rule. Record feature-specific decisions with their feature. For sustained work, keep a task checkpoint with the code revision, verified states, evidence, and outstanding work. Include the branch or PR, checkpoint path, and localhost route in a handoff. On resumption, compare the checkpoint with the current branch, working tree, and relevant live sources before acting. diff --git a/.agents/skills/implement-platform/agents/openai.yaml b/.agents/skills/implement-platform/agents/openai.yaml new file mode 100644 index 00000000000..7f4a26951bd --- /dev/null +++ b/.agents/skills/implement-platform/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Implement Sim Platform" + short_description: "Build and review the code-first Sim Platform" + default_prompt: "Use $implement-platform to implement the next Platform product change in Sim." diff --git a/.claude/rules/emcn-components.md b/.claude/rules/emcn-components.md index 6ed0a0e1d20..28a6db5abd5 100644 --- a/.claude/rules/emcn-components.md +++ b/.claude/rules/emcn-components.md @@ -6,6 +6,8 @@ paths: # EMCN Components +This guide applies to the existing app. For `apps/sim/platform/**`, use `apps/sim/platform/AGENTS.md`. + Import components, `cn`, and tokens from the `@sim/emcn` barrel; icons come from the `@sim/emcn/icons` subpath, and CSS modules from their file path. Never deep-import other component subpaths. The **chip family** is the platform's primary chrome — always reach for it over the legacy primitives it is progressively replacing (`Input`→`ChipInput`, `Textarea`→`ChipTextarea`, `Modal`→`ChipModal`, `Select`/`Combobox`→`ChipSelect`/`ChipCombobox`/`ChipDropdown`, date field→`ChipDatePicker`). `ChipSwitch` is a segmented choice between options, not a replacement for `Switch`: a boolean on/off toggle stays `Switch`. For context/action menus the canonical control is `DropdownMenu` — the standard menu (not a chip, and never a hand-rolled popover). ## Chip chrome — single source of truth diff --git a/.claude/rules/platform-design.md b/.claude/rules/platform-design.md new file mode 100644 index 00000000000..32f7a2dd866 --- /dev/null +++ b/.claude/rules/platform-design.md @@ -0,0 +1,7 @@ +--- +description: Sim Platform implementation from Paper +paths: + - "apps/sim/platform/**" +--- + +Use the canonical contract in `apps/sim/platform/AGENTS.md` and the `implement-platform` skill. Legacy EMCN and app styling rules do not govern this separate project. diff --git a/.claude/rules/sim-architecture.md b/.claude/rules/sim-architecture.md index fd1bda06f19..4cc7f8b423c 100644 --- a/.claude/rules/sim-architecture.md +++ b/.claude/rules/sim-architecture.md @@ -6,6 +6,8 @@ paths: # Sim App Architecture +This guide applies to the existing app. For `apps/sim/platform/**`, use `apps/sim/platform/AGENTS.md`. + Repository layout, package boundaries, the application operation boundary, naming, and utils placement are in the root `CLAUDE.md`. This file holds the app-internal boundaries. ## The `'use client'` server boundary diff --git a/.claude/rules/sim-platform-views.md b/.claude/rules/sim-platform-views.md new file mode 100644 index 00000000000..04061b6aba5 --- /dev/null +++ b/.claude/rules/sim-platform-views.md @@ -0,0 +1,72 @@ +--- +description: One pattern per job across the organization views (Settings, People, Integrations, Links, Home) +paths: + - "apps/sim/platform/embedded/app/(shell)/{settings,people,integrations,links,home}/**" + - "apps/sim/platform/embedded/lib/**" + - "apps/sim/platform/shared/**" + - "apps/sim/platform/design-system/components/{status,modal,picker,settings-section,search-field,activity-row}/**" + - "apps/sim/app/o/*/**" + - "apps/sim/app/o/*/settings/**" +--- + +# Organization Views + +The organization views live in `apps/sim/platform/embedded/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`. +- `(shell)/_components/section-parts/` holds what settings and the secrets page share: `SectionStack`, `FieldStack`, `SectionToolbar`, + `DetailHeader`, `HeadingRow`, `Muted`, `SectionSkeleton`, `DraftConflictNotice`. `useServerDraft` is in `_components/`, since Integrations uses it too. + +## Non-UI code lives once + +A view's data and logic belong in `apps/sim/platform/embedded/lib/data` when only these views use them, and in `apps/sim/platform/shared` when the zone has the same thing. A view reads hooks through that layer; a real adapter owns the mutation, its toasts and any mapping, and is never a re-export. Before adding a file to `embedded/`, look for its counterpart under `apps/sim/platform/lib` and follow "Converging a domain" in `apps/sim/platform/AGENTS.md`. `check:platform-convergence` fails a new file here or a new path both trees hold. + +## 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..8d081fa3d4f 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}/**" @@ -344,7 +343,7 @@ rendered through `` from is what keeps tone/icon/variant/tooltip handling from drifting between the two shells. Reach for it before hand-rolling a `Chip`. - **`useSettingsUnsavedGuard({ isDirty, navigationBlocked, onDiscard })`** from - `@/components/settings/use-settings-unsaved-guard` registers one editor with the + `@/hooks/use-settings-unsaved-guard` registers one editor with the shared settings store. Register every full-page draft, including inline creation forms. Dirty computation stays local and derived; fetched defaults stay clean. - **`onDiscard` resets the registered draft.** The root `SettingsNavigationGuard` diff --git a/.claude/rules/sim-styling.md b/.claude/rules/sim-styling.md index 2c5e16be354..613f0443a97 100644 --- a/.claude/rules/sim-styling.md +++ b/.claude/rules/sim-styling.md @@ -8,6 +8,8 @@ paths: # Styling Rules +This guide applies to the existing app. For `apps/sim/platform/**`, use `apps/sim/platform/AGENTS.md`. + ## Tailwind 1. **No inline styles** - Use Tailwind classes. Exception: a genuinely dynamic diff --git a/.claude/rules/sim-url-state.md b/.claude/rules/sim-url-state.md index 914bb12403c..110aad206f4 100644 --- a/.claude/rules/sim-url-state.md +++ b/.claude/rules/sim-url-state.md @@ -179,7 +179,7 @@ const setSearch = useDebouncedSearchSetter(write, { debounceMs: 200 }) - **Never write a trimmed value to a param that controls the input** — trimming on write eats the user's trailing space mid-typing and makes multi-word queries untypable. The hook writes the raw value; trim only for the empty-check (the hook does this) and on *read* where the value feeds a query or filter. - **Keep fetches/filtering debounced.** Where the search value feeds a React Query key or an expensive in-memory filter, derive a debounced value off the instant nuqs value (`const debounced = useDebounce(urlSearch, SEARCH_DEBOUNCE_MS)` with `SEARCH_DEBOUNCE_MS` from `@/lib/url-state`) and feed *that* to the query — the instant value is only for the input box. Cheap in-memory filtering over a small static list may read the instant value directly. -- Settings list search boxes use `useSettingsSearch()` from `settings/components/use-settings-search` — the shared `?search=` binding for that surface. +- Settings list search boxes use `useSettingsSearch()` from `@/hooks/use-settings-search` — the shared `?search=` binding for that surface. - Preserve `clearOnDefault` (empty clears the param), the existing default, and `history: 'replace'`. See logs (`use-log-filters.ts` grouped, query stays debounced), integrations (cheap in-memory filter, instant value), and tables (filter stays debounced). ## Sort convention (`sort` + `dir`) diff --git a/.claude/skills/implement-platform b/.claude/skills/implement-platform new file mode 120000 index 00000000000..09d30a82d38 --- /dev/null +++ b/.claude/skills/implement-platform @@ -0,0 +1 @@ +../../.agents/skills/implement-platform \ No newline at end of file diff --git a/.cursor/rules/emcn-components.mdc b/.cursor/rules/emcn-components.mdc index 146915df09d..152cfcbdf52 100644 --- a/.cursor/rules/emcn-components.mdc +++ b/.cursor/rules/emcn-components.mdc @@ -7,6 +7,8 @@ globs: ["packages/emcn/**"] # EMCN Components +This guide applies to the existing app. For `apps/sim/platform/**`, use `apps/sim/platform/AGENTS.md`. + Import components, `cn`, and tokens from the `@sim/emcn` barrel; icons come from the `@sim/emcn/icons` subpath, and CSS modules from their file path. Never deep-import other component subpaths. The **chip family** is the platform's primary chrome — always reach for it over the legacy primitives it is progressively replacing (`Input`→`ChipInput`, `Textarea`→`ChipTextarea`, `Modal`→`ChipModal`, `Select`/`Combobox`→`ChipSelect`/`ChipCombobox`/`ChipDropdown`, date field→`ChipDatePicker`). `ChipSwitch` is a segmented choice between options, not a replacement for `Switch`: a boolean on/off toggle stays `Switch`. For context/action menus the canonical control is `DropdownMenu` — the standard menu (not a chip, and never a hand-rolled popover). ## Chip chrome — single source of truth diff --git a/.cursor/rules/platform-design.mdc b/.cursor/rules/platform-design.mdc new file mode 100644 index 00000000000..d84b752bf06 --- /dev/null +++ b/.cursor/rules/platform-design.mdc @@ -0,0 +1,8 @@ +--- +description: "Sim Platform implementation from Paper" +globs: ["apps/sim/platform/**"] +--- + + + +Use the canonical contract in `apps/sim/platform/AGENTS.md` and the `implement-platform` skill. Legacy EMCN and app styling rules do not govern this separate project. diff --git a/.cursor/rules/sim-architecture.mdc b/.cursor/rules/sim-architecture.mdc index 740e9601df1..a3f69a1605e 100644 --- a/.cursor/rules/sim-architecture.mdc +++ b/.cursor/rules/sim-architecture.mdc @@ -7,6 +7,8 @@ globs: ["apps/sim/**"] # Sim App Architecture +This guide applies to the existing app. For `apps/sim/platform/**`, use `apps/sim/platform/AGENTS.md`. + Repository layout, package boundaries, the application operation boundary, naming, and utils placement are in the root `CLAUDE.md`. This file holds the app-internal boundaries. ## The `'use client'` server boundary diff --git a/.cursor/rules/sim-platform-views.mdc b/.cursor/rules/sim-platform-views.mdc new file mode 100644 index 00000000000..2459c7e5cc3 --- /dev/null +++ b/.cursor/rules/sim-platform-views.mdc @@ -0,0 +1,68 @@ +--- +description: "One pattern per job across the organization views (Settings, People, Integrations, Links, Home)" +globs: ["apps/sim/platform/embedded/app/(shell)/{settings,people,integrations,links,home}/**","apps/sim/platform/embedded/lib/**","apps/sim/platform/shared/**","apps/sim/platform/design-system/components/{status,modal,picker,settings-section,search-field,activity-row}/**","apps/sim/app/o/*/**","apps/sim/app/o/*/settings/**"] +--- + + + +# Organization Views + +The organization views live in `apps/sim/platform/embedded/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`. +- `(shell)/_components/section-parts/` holds what settings and the secrets page share: `SectionStack`, `FieldStack`, `SectionToolbar`, + `DetailHeader`, `HeadingRow`, `Muted`, `SectionSkeleton`, `DraftConflictNotice`. `useServerDraft` is in `_components/`, since Integrations uses it too. + +## Non-UI code lives once + +A view's data and logic belong in `apps/sim/platform/embedded/lib/data` when only these views use them, and in `apps/sim/platform/shared` when the zone has the same thing. A view reads hooks through that layer; a real adapter owns the mutation, its toasts and any mapping, and is never a re-export. Before adding a file to `embedded/`, look for its counterpart under `apps/sim/platform/lib` and follow "Converging a domain" in `apps/sim/platform/AGENTS.md`. `check:platform-convergence` fails a new file here or a new path both trees hold. + +## 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..565e511c069 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/**"] --- @@ -340,7 +340,7 @@ rendered through `` from is what keeps tone/icon/variant/tooltip handling from drifting between the two shells. Reach for it before hand-rolling a `Chip`. - **`useSettingsUnsavedGuard({ isDirty, navigationBlocked, onDiscard })`** from - `@/components/settings/use-settings-unsaved-guard` registers one editor with the + `@/hooks/use-settings-unsaved-guard` registers one editor with the shared settings store. Register every full-page draft, including inline creation forms. Dirty computation stays local and derived; fetched defaults stay clean. - **`onDiscard` resets the registered draft.** The root `SettingsNavigationGuard` diff --git a/.cursor/rules/sim-styling.mdc b/.cursor/rules/sim-styling.mdc index f016cfc5b3f..7cef8bf6655 100644 --- a/.cursor/rules/sim-styling.mdc +++ b/.cursor/rules/sim-styling.mdc @@ -7,6 +7,8 @@ globs: ["apps/sim/**/*.tsx","apps/sim/**/*.css","packages/workflow-renderer/**/* # Styling Rules +This guide applies to the existing app. For `apps/sim/platform/**`, use `apps/sim/platform/AGENTS.md`. + ## Tailwind 1. **No inline styles** - Use Tailwind classes. Exception: a genuinely dynamic diff --git a/.cursor/rules/sim-url-state.mdc b/.cursor/rules/sim-url-state.mdc index cb1d05fa410..7b420a26588 100644 --- a/.cursor/rules/sim-url-state.mdc +++ b/.cursor/rules/sim-url-state.mdc @@ -172,7 +172,7 @@ const setSearch = useDebouncedSearchSetter(write, { debounceMs: 200 }) - **Never write a trimmed value to a param that controls the input** — trimming on write eats the user's trailing space mid-typing and makes multi-word queries untypable. The hook writes the raw value; trim only for the empty-check (the hook does this) and on *read* where the value feeds a query or filter. - **Keep fetches/filtering debounced.** Where the search value feeds a React Query key or an expensive in-memory filter, derive a debounced value off the instant nuqs value (`const debounced = useDebounce(urlSearch, SEARCH_DEBOUNCE_MS)` with `SEARCH_DEBOUNCE_MS` from `@/lib/url-state`) and feed *that* to the query — the instant value is only for the input box. Cheap in-memory filtering over a small static list may read the instant value directly. -- Settings list search boxes use `useSettingsSearch()` from `settings/components/use-settings-search` — the shared `?search=` binding for that surface. +- Settings list search boxes use `useSettingsSearch()` from `@/hooks/use-settings-search` — the shared `?search=` binding for that surface. - Preserve `clearOnDefault` (empty clears the param), the existing default, and `history: 'replace'`. See logs (`use-log-filters.ts` grouped, query stays debounced), integrations (cheap in-memory filter, instant value), and tables (filter stays debounced). ## Sort convention (`sort` + `dir`) diff --git a/.github/CI.md b/.github/CI.md index 1831aefbb53..f71d95dc506 100644 --- a/.github/CI.md +++ b/.github/CI.md @@ -40,8 +40,14 @@ WebKit acceptance on Blacksmith uses the matching, digest-pinned Playwright brow HTTP and docs browser installation first asks the pinned Playwright driver to simulate its OS dependency installation. When the simulation requires no package changes, the runner avoids an apt update; missing dependencies, proposed upgrades, or an unsuccessful simulation use the normal dependency installer. Browser selection and downloads still use the pinned driver, including full native Chromium for extension acceptance. +`app/o` bundles `platform/embedded` and `platform/design-system`, so both are build and `ci:shared` inputs: an edit to either changes the build hash and selects every boundary. The Platform zone (`platform/{app,lib,fixtures,checks,docs,preview,public}` and its own configs) is a separate Next app that the embedded tree may not import, reached only through the `/org` rewrite, and is excluded by name so that a new `platform/` folder stays an input until it is declared zone-only. A zone-only edit selects `platform-design.yml` and the app's unit tests, and no main-app build or acceptance job. + Every HTTP and browser group consumes the same production build in an isolated job/database. Electron also uses that build and skips its development-route warm-up. Local HTTP and Electron development runs retain their dev-server behavior. Build inputs exclude test-only files so a harness edit can reuse an unchanged production build. +The Platform's fixture and live suites stay in `platform-design.yml`, outside `checks / ci`. The HTTP and production matrices consume one `next-build` artifact, built without `SIM_PLATFORM_ORIGIN`, while the `zone` topology bakes that origin into its rewrite and also builds the Platform app, so it needs its own job, gate flag and plan output rather than a suite entry. A task that depends on `ci:shared` would select two 16-vCPU jobs of up to 75 minutes for nearly every app PR and hold every staging push's deployment gate on them. The cost is that a red run is advisory, and a path-filtered workflow cannot be a required check: one that never starts never reports, so the status does not block a merge until the suite joins the aggregate. + +The `zone` job runs its build through several phases on one database: the suite, the zone gone, the zone answering 503, the zone back, the flag off, flags read from a fixture of AppConfig's service with one organization targeted, and misconfigured zones that must refuse to start. The `flag-off` job runs its no-zone build, then the same build self-hosted. The two cannot read each other's reports, so `check:platform-parity --job` holds each to its own rows. + Desktop inbox protocol, assistant actions UI, and organization search UI run as three parallel HTTP jobs with isolated databases, apps, and reports. Each harness owns its acceptance task, so an edit selects its own job. The inbox protocol task depends on server and realtime code; the browser tasks also depend on workspace UI. Desktop inbox needs no browser. The UI jobs and browser extension install Chromium while their app starts, waiting for both before executing assertions; browser-install failures still fail the job and preserve the install log. No acceptance assertions are removed by the split. Live Electron acceptance tracks its live-app suite, fixtures, and Playwright configuration separately from shell-only smoke tests. Changing a shell-only smoke case runs the dedicated desktop workflow without starting the app's production build and live lease checks. 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..c35024f78ee 100644 --- a/.github/scripts/ci-plan.gql +++ b/.github/scripts/ci-plan.gql @@ -1,24 +1,60 @@ -query($base: String!, $head: String!) { - packages { items { name path } } - 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-docs-platform", - "ci:e2e-browser-extension", "ci:e2e-project-files", "ci:e2e-mobile", "ci:e2e-seo" - ]) { +query ($base: String!, $head: String!) { + packages { + items { + name + path + } + } + 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-org-redesign" + "ci:e2e-docs-platform" + "ci:e2e-browser-extension" + "ci:e2e-project-files" + "ci:e2e-mobile" + "ci:e2e-seo" + ] + ) { items { name fullName - package { name } + package { + name + } reason { __typename - ... on TaskFileChanged { filePath } - ... on TaskGlobalFileChanged { filePath } - ... on TaskGlobalDepsChanged { filePath } - ... on TaskDependencyTaskChanged { taskName packageName } - ... on TaskPackageDependencyChanged { packageName } - ... on TaskAllChanged { description } + ... on TaskFileChanged { + filePath + } + ... on TaskGlobalFileChanged { + filePath + } + ... on TaskGlobalDepsChanged { + filePath + } + ... on TaskDependencyTaskChanged { + taskName + packageName + } + ... on TaskPackageDependencyChanged { + packageName + } + ... on TaskAllChanged { + description + } } } } diff --git a/.github/scripts/http-e2e.sh b/.github/scripts/http-e2e.sh index 8c5f185e7cd..e5d1de0cf9e 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,12 @@ status_log='' realtime_pid='' realtime_tag='' realtime_log='' +platform_pid='' +platform_tag='' +platform_log='' +agent_pid='' +failing_zone_pid='' +appconfig_pid='' browsers_pid='' local_app_url='' # `dev` or `start`; a group sets it before start_app. @@ -63,6 +69,25 @@ finish() { tail -n 100 "$realtime_log" fi fi + if [ -n "$platform_pid" ]; then + bash "$GITHUB_WORKSPACE/.github/scripts/stop-session.sh" "$platform_pid" "$platform_tag" || status=1 + wait "$platform_pid" 2>/dev/null || true + if [ "$status" -ne 0 ]; then + tail -n 100 "$platform_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 "$failing_zone_pid" ]; then + kill "$failing_zone_pid" 2>/dev/null || true + wait "$failing_zone_pid" 2>/dev/null || true + fi + if [ -n "$appconfig_pid" ]; then + kill "$appconfig_pid" 2>/dev/null || true + wait "$appconfig_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 +158,86 @@ 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='' +} + +# start_platform : the production build of apps/sim/platform, behind the main app's rewrite. +start_platform() { + local port=$1 + platform_tag="platform-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT-$$" + platform_log="$report_dir/platform-live-platform.log" + E2E_APP="$platform_tag" SIM_PLATFORM_API_ORIGIN="$local_app_url" SIM_PLATFORM_BASE_PATH=/org setsid node \ + ../../node_modules/next/dist/bin/next start platform --hostname 127.0.0.1 --port "$port" \ + >> "$platform_log" 2>&1 & + platform_pid=$! + wait_ready "$platform_pid" "http://127.0.0.1:$port/org" 'Platform zone' +} + +# stop_platform: takes the zone away from under a running main app, as an outage does. +stop_platform() { + bash "$GITHUB_WORKSPACE/.github/scripts/stop-session.sh" "$platform_pid" "$platform_tag" + wait "$platform_pid" 2>/dev/null || true + platform_pid='' +} + +# platform_live_environment: one hosted Enterprise organization, the deterministic seed its specs +# read, and a scripted worker in place of the model. Hosted is decided by the app's hostname, which +# the job maps to loopback. Google and Microsoft answer only to real credentials, so those clients +# are fixtures, and what no fixture can reach is reported by the specs as a skip. Each group starts +# the realtime server after its app, as org-redesign does, and holds the same tripwire and tool +# permissions, so a route that passes one suite cannot fail on the same stack in the other. +platform_live_environment() { + app_server=start + export REDIS_URL=redis://127.0.0.1:6379 + export E2E_APP_PUBLIC_URL=http://platform-live.sim.ai:3027 + export NEXT_PUBLIC_APP_URL="$E2E_APP_PUBLIC_URL" + export INTERNAL_API_BASE_URL=http://127.0.0.1:3027 TRUSTED_ORIGINS=http://127.0.0.1:3027 + export BILLING_ENABLED=true NEXT_PUBLIC_BILLING_ENABLED=true + export ENTERPRISE_ENABLED=true NEXT_PUBLIC_ENTERPRISE_ENABLED=true + export SSO_ENABLED=true NEXT_PUBLIC_SSO_ENABLED=true + export SCIM_ENABLED=true NEXT_PUBLIC_SCIM_ENABLED=true + export ORGANIZATIONS_ENABLED=true NEXT_PUBLIC_ORGANIZATIONS_ENABLED=true + export KNOWLEDGE_MEMBER_ACCESS=true CREDENTIAL_GROUPS=true ACCESS_CONTROL_ENABLED=true + export NEXT_PUBLIC_ACCESS_CONTROL_ENABLED=true + export SIM_LINKS=true PEOPLE=true + export INTERNAL_API_SECRET=platform-live-ci-local-secret-at-least-32-characters + export COPILOT_TOOL_PERMISSIONS_ENABLED=true + export DB_TX_TRIPWIRE=throw + export NEXT_PUBLIC_SOCKET_URL=http://127.0.0.1:3030 SOCKET_SERVER_URL=http://127.0.0.1:3030 + export COPILOT_API_KEY=platform-live-ci-local-copilot-key MOTHERSHIP_SIM_TRANSPORT=direct + export SIM_AGENT_API_URL=http://127.0.0.1:3029 + export GOOGLE_CLIENT_ID=platform-live-google-client GOOGLE_CLIENT_SECRET=platform-live-google-secret + export MICROSOFT_CLIENT_ID=platform-live-microsoft-client MICROSOFT_CLIENT_SECRET=platform-live-microsoft-secret + export STRIPE_SECRET_KEY=sk_test_platform_live STRIPE_WEBHOOK_SECRET=whsec_platform_live + + export PLATFORM_LIVE_ORIGIN="$E2E_APP_PUBLIC_URL" + export PLATFORM_LIVE_PASSWORD=platform-live-password-1 + export PLATFORM_LIVE_ORGANIZATION_ID=platform-live-organization + export PLATFORM_LIVE_ORGANIZATION_NAME='Sim Live Org' + export PLATFORM_LIVE_OWNER_EMAIL=olive@platform-live.test + export PLATFORM_LIVE_EMAIL=ada@platform-live.test + export PLATFORM_LIVE_MEMBER_EMAIL=mia@platform-live.test + export PLATFORM_LIVE_CONNECTED_EMAIL=cory@platform-live.test + export PLATFORM_LIVE_ROLE_TARGET_EMAIL=rae@platform-live.test + export PLATFORM_LIVE_REMOVABLE_EMAIL=remy@platform-live.test + export PLATFORM_LIVE_REFUSED_INVITE_EMAIL=otto@platform-live.test + export PLATFORM_LIVE_OFF_ORGANIZATION_ID=platform-live-organization-off + export PLATFORM_LIVE_OFF_EMAIL=odessa@platform-live.test + export PLATFORM_LIVE_ORCHESTRATOR=1 + bun --no-env-file scripts/platform-live-seed.ts +} + +# run_platform_live +run_platform_live() { + local project=$1 name=$2 + PLATFORM_LIVE_REPORT_PATH="$report_dir/$name/report.json" \ + bun run test:platform:live --project="$project" ${PLATFORM_LIVE_GREP:+--grep "$PLATFORM_LIVE_GREP"} +} + # wait_ready