diff --git a/.agents/skills/implement-platform/SKILL.md b/.agents/skills/implement-platform/SKILL.md index 92833c90da7..001f7dd2d29 100644 --- a/.agents/skills/implement-platform/SKILL.md +++ b/.agents/skills/implement-platform/SKILL.md @@ -10,8 +10,8 @@ Start in the Sim checkout containing the requested product work. Read the [Platf 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. +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. Toolbar choices use ChoiceMenu, as Home's period does; Select/SelectField belong to forms and settings. Compare the rendered trigger, not just its variant name. Put unique anatomy and its CSS together in `_components//.tsx` and `.module.css`. Match an existing semantic rhythm, such as Home's ActivityRow, for a custom composition. 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. Before adding a shared visual variant, name at least two independent real consumers. A single feature composes the existing primitives in its local owner; a label, icon, data value or responsive arrangement does not justify another variant. Keep the public API small enough to understand at the call site. 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. Reuse the single review server at localhost:3125. Check its process working directory and branch before starting a server or moving work. Separate task worktrees do not update that preview until reconciled into its checkout. Provide the working 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/apps/sim/app/workspace/[workspaceId]/components/integration-tabs-header/integration-tabs-header.tsx b/apps/sim/app/workspace/[workspaceId]/components/integration-tabs-header/integration-tabs-header.tsx index 11774143550..f361ec4191b 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/integration-tabs-header/integration-tabs-header.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/integration-tabs-header/integration-tabs-header.tsx @@ -3,6 +3,8 @@ import type { ReactNode } from 'react' import { ChipLink, cn } from '@sim/emcn' import { HEADER_ACTION_CLUSTER, PAGE_HEADER_BAR } from '@/components/page-header-bar' +import { workspaceIntegrationsPath } from '@/lib/navigation/paths' +import { useWorkspaceHostContext } from '@/app/workspace/[workspaceId]/providers/workspace-host-provider' interface IntegrationTabsHeaderProps { active: 'integrations' | 'skills' @@ -17,9 +19,13 @@ export function IntegrationTabsHeader({ workspaceId, rightSlot, }: IntegrationTabsHeaderProps) { + const host = useWorkspaceHostContext() return (
- + Integrations diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/list-chrome.ts b/apps/sim/app/workspace/[workspaceId]/components/resource/list-chrome.ts new file mode 100644 index 00000000000..7fb77faa655 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/components/resource/list-chrome.ts @@ -0,0 +1,110 @@ +import type { ComponentType } from 'react' +import { + Database, + Download, + File, + FolderPlus, + Library, + Plus, + RefreshCw, + Table, + Upload, +} from '@sim/emcn/icons' +import { FOLDERED_RESOURCE_HEADERS } from '@/app/workspace/[workspaceId]/components/folders/foldered-resources' +import type { ChromeActionSpec } from '@/app/workspace/[workspaceId]/components/resource/components/resource-chrome-fallback' +import type { ResourceColumn } from '@/app/workspace/[workspaceId]/components/resource/resource' + +interface ListChrome { + title: string + icon: ComponentType<{ className?: string }> + columns: readonly ResourceColumn[] + actions: readonly ChromeActionSpec[] + searchPlaceholder: string + hasSort: boolean + hasFilter: boolean +} + +/** The original list chrome used for loading and for fixture rendering of a workspace page. */ +export const RESOURCE_LIST_CHROME = { + tables: { + title: FOLDERED_RESOURCE_HEADERS.table.rootLabel, + icon: Table, + columns: [ + { id: 'name', header: 'Name' }, + { id: 'columns', header: 'Columns' }, + { id: 'rows', header: 'Rows' }, + { id: 'created', header: 'Created' }, + { id: 'owner', header: 'Owner' }, + { id: 'updated', header: 'Last Updated' }, + ], + actions: [ + { text: 'Import CSV', icon: Upload }, + { text: 'New folder', icon: FolderPlus }, + { text: 'New table', icon: Plus, variant: 'primary' }, + ], + searchPlaceholder: 'Search tables...', + hasSort: true, + hasFilter: true, + }, + files: { + title: FOLDERED_RESOURCE_HEADERS.file.rootLabel, + icon: File, + columns: [ + { id: 'name', header: 'Name', widthMultiplier: 1.15 }, + { id: 'size', header: 'Size', widthMultiplier: 0.85 }, + { id: 'type', header: 'Type', widthMultiplier: 1.0 }, + { id: 'created', header: 'Created' }, + { id: 'owner', header: 'Owner' }, + { id: 'updated', header: 'Last Updated' }, + ], + actions: [ + { text: 'Upload', icon: Upload }, + { text: 'New folder', icon: FolderPlus }, + { text: 'New file', icon: Plus, variant: 'primary' }, + ], + searchPlaceholder: 'Search files...', + hasSort: true, + hasFilter: true, + }, + knowledge: { + title: FOLDERED_RESOURCE_HEADERS.knowledge_base.rootLabel, + icon: Database, + columns: [ + { id: 'name', header: 'Name' }, + { id: 'documents', header: 'Documents', widthMultiplier: 0.6 }, + { id: 'tokens', header: 'Tokens', widthMultiplier: 0.6 }, + { id: 'connectors', header: 'Connectors', widthMultiplier: 0.7 }, + { id: 'created', header: 'Created' }, + { id: 'owner', header: 'Owner' }, + { id: 'updated', header: 'Last Updated' }, + ], + actions: [ + { text: 'New folder', icon: FolderPlus }, + { text: 'New base', icon: Plus, variant: 'primary' }, + ], + searchPlaceholder: 'Search knowledge bases...', + hasSort: true, + hasFilter: true, + }, + logs: { + title: 'Logs', + icon: Library, + columns: [ + { id: 'workflow', header: 'Workflow' }, + { id: 'date', header: 'Date' }, + { id: 'status', header: 'Status' }, + { id: 'cost', header: 'Cost' }, + { id: 'trigger', header: 'Trigger' }, + { id: 'duration', header: 'Duration' }, + ], + actions: [ + { text: 'Export', icon: Download }, + { text: 'Refresh', icon: RefreshCw }, + { text: 'Logs', active: true }, + { text: 'Dashboard' }, + ], + searchPlaceholder: 'Search logs...', + hasSort: true, + hasFilter: true, + }, +} as const satisfies Record diff --git a/apps/sim/app/workspace/[workspaceId]/files/loading.tsx b/apps/sim/app/workspace/[workspaceId]/files/loading.tsx index 1e610f525c0..71c1454c1b3 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/loading.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/loading.tsx @@ -1,36 +1,8 @@ 'use client' -import { File as FilesIcon, FolderPlus, Plus, Upload } from '@sim/emcn' -import { - type ChromeActionSpec, - ResourceChromeFallback, -} from '@/app/workspace/[workspaceId]/components/resource/components/resource-chrome-fallback' - -const COLUMNS = [ - { id: 'name', header: 'Name', widthMultiplier: 1.15 }, - { id: 'size', header: 'Size', widthMultiplier: 0.85 }, - { id: 'type', header: 'Type', widthMultiplier: 1.0 }, - { id: 'created', header: 'Created' }, - { id: 'owner', header: 'Owner' }, - { id: 'updated', header: 'Last Updated' }, -] - -const ACTIONS: ChromeActionSpec[] = [ - { text: 'Upload', icon: Upload }, - { text: 'New folder', icon: FolderPlus }, - { text: 'New file', icon: Plus, variant: 'primary' }, -] +import { ResourceChromeFallback } from '@/app/workspace/[workspaceId]/components/resource/components/resource-chrome-fallback' +import { RESOURCE_LIST_CHROME } from '@/app/workspace/[workspaceId]/components/resource/list-chrome' export default function FilesLoading() { - return ( - - ) + return } diff --git a/apps/sim/app/workspace/[workspaceId]/integrations/integrations.tsx b/apps/sim/app/workspace/[workspaceId]/integrations/integrations.tsx index 7817b234ae4..226f2803cc8 100644 --- a/apps/sim/app/workspace/[workspaceId]/integrations/integrations.tsx +++ b/apps/sim/app/workspace/[workspaceId]/integrations/integrations.tsx @@ -12,7 +12,7 @@ import { Search, } from '@sim/emcn' import { Lock } from '@sim/emcn/icons' -import { useParams } from 'next/navigation' +import { redirect, useParams } from 'next/navigation' import { useQueryStates } from 'nuqs' import { blockTypeToIconMap, @@ -22,6 +22,7 @@ import { resolveCredentialDisplay, } from '@/lib/integrations' import { isIntegrationPermitted } from '@/lib/integrations/access' +import { workspaceIntegrationsPath } from '@/lib/navigation/paths' import { IntegrationTabsHeader } from '@/app/workspace/[workspaceId]/components/integration-tabs-header' import { IntegrationSection } from '@/app/workspace/[workspaceId]/integrations/components/integration-section' import { IntegrationTile } from '@/app/workspace/[workspaceId]/integrations/components/integrations-showcase' @@ -34,6 +35,7 @@ import { integrationsParsers, integrationsUrlKeys, } from '@/app/workspace/[workspaceId]/integrations/search-params' +import { useWorkspaceHostContext } from '@/app/workspace/[workspaceId]/providers/workspace-host-provider' import { SettingsEmptyState, SettingsQueryErrorState, @@ -159,6 +161,10 @@ function ConnectedItem({ href, blockType, name, description, icon: Icon }: Conne } export function Integrations() { + const host = useWorkspaceHostContext() + const workspaceId = host.workspace.id + const destination = workspaceIntegrationsPath(workspaceId, host) + if (destination !== `/workspace/${workspaceId}/integrations`) redirect(destination) return ( diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/loading.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/loading.tsx index 0ebfc2c04da..020e17eb5fc 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/loading.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/loading.tsx @@ -1,39 +1,8 @@ 'use client' -import { Database, FolderPlus, Plus } from '@sim/emcn/icons' -import { FOLDERED_RESOURCE_HEADERS } from '@/app/workspace/[workspaceId]/components/folders/foldered-resources' -import { - type ChromeActionSpec, - ResourceChromeFallback, -} from '@/app/workspace/[workspaceId]/components/resource/components/resource-chrome-fallback' - -const KNOWLEDGE_HEADER = FOLDERED_RESOURCE_HEADERS.knowledge_base - -const COLUMNS = [ - { id: 'name', header: 'Name' }, - { id: 'documents', header: 'Documents', widthMultiplier: 0.6 }, - { id: 'tokens', header: 'Tokens', widthMultiplier: 0.6 }, - { id: 'connectors', header: 'Connectors', widthMultiplier: 0.7 }, - { id: 'created', header: 'Created' }, - { id: 'owner', header: 'Owner' }, - { id: 'updated', header: 'Last Updated' }, -] - -const ACTIONS: ChromeActionSpec[] = [ - { text: 'New folder', icon: FolderPlus }, - { text: 'New base', icon: Plus, variant: 'primary' }, -] +import { ResourceChromeFallback } from '@/app/workspace/[workspaceId]/components/resource/components/resource-chrome-fallback' +import { RESOURCE_LIST_CHROME } from '@/app/workspace/[workspaceId]/components/resource/list-chrome' export default function KnowledgeLoading() { - return ( - - ) + return } diff --git a/apps/sim/app/workspace/[workspaceId]/logs/loading.tsx b/apps/sim/app/workspace/[workspaceId]/logs/loading.tsx index be474fb3b62..89c52eb529a 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/loading.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/loading.tsx @@ -1,38 +1,8 @@ 'use client' -import { Library, RefreshCw } from '@sim/emcn' -import { Download } from '@sim/emcn/icons' -import { - type ChromeActionSpec, - ResourceChromeFallback, -} from '@/app/workspace/[workspaceId]/components/resource/components/resource-chrome-fallback' - -const COLUMNS = [ - { id: 'workflow', header: 'Workflow' }, - { id: 'date', header: 'Date' }, - { id: 'status', header: 'Status' }, - { id: 'cost', header: 'Cost' }, - { id: 'trigger', header: 'Trigger' }, - { id: 'duration', header: 'Duration' }, -] - -const ACTIONS: ChromeActionSpec[] = [ - { text: 'Export', icon: Download }, - { text: 'Refresh', icon: RefreshCw }, - { text: 'Logs', active: true }, - { text: 'Dashboard' }, -] +import { ResourceChromeFallback } from '@/app/workspace/[workspaceId]/components/resource/components/resource-chrome-fallback' +import { RESOURCE_LIST_CHROME } from '@/app/workspace/[workspaceId]/components/resource/list-chrome' export default function LogsLoading() { - return ( - - ) + return } diff --git a/apps/sim/app/workspace/[workspaceId]/tables/loading.tsx b/apps/sim/app/workspace/[workspaceId]/tables/loading.tsx index 6b6dc1bd7cd..458cf936edc 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/loading.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/loading.tsx @@ -1,37 +1,8 @@ 'use client' -import { Plus, Upload } from '@sim/emcn' -import { FolderPlus, Table as TableIcon } from '@sim/emcn/icons' -import { - type ChromeActionSpec, - ResourceChromeFallback, -} from '@/app/workspace/[workspaceId]/components/resource/components/resource-chrome-fallback' - -const COLUMNS = [ - { id: 'name', header: 'Name' }, - { id: 'columns', header: 'Columns' }, - { id: 'rows', header: 'Rows' }, - { id: 'created', header: 'Created' }, - { id: 'owner', header: 'Owner' }, - { id: 'updated', header: 'Last Updated' }, -] - -const ACTIONS: ChromeActionSpec[] = [ - { text: 'Import CSV', icon: Upload }, - { text: 'New folder', icon: FolderPlus }, - { text: 'New table', icon: Plus, variant: 'primary' }, -] +import { ResourceChromeFallback } from '@/app/workspace/[workspaceId]/components/resource/components/resource-chrome-fallback' +import { RESOURCE_LIST_CHROME } from '@/app/workspace/[workspaceId]/components/resource/list-chrome' export default function TablesLoading() { - return ( - - ) + return } diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx index a31b13f51f7..76d18074f06 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx @@ -45,11 +45,13 @@ import { useDeploymentShape } from '@/lib/core/config/deployment-shape' import { MothershipHandoffStorage } from '@/lib/core/utils/browser-storage' import { getFolderPathNames } from '@/lib/folders/tree' import { sendMothershipMessage } from '@/lib/mothership/events' +import { workspaceIntegrationsPath } from '@/lib/navigation/paths' import { captureEvent } from '@/lib/posthog/client' import { toSearchToken } from '@/lib/search/tokens' import { hasTriggerCapability } from '@/lib/workflows/triggers/utils' import { parseWorkspaceFileFolderDisplayPath } from '@/lib/workspace-files/folder-display-path' import { useInvokeGlobalCommand } from '@/app/workspace/[workspaceId]/providers/global-commands-provider' +import { useWorkspaceHostContext } from '@/app/workspace/[workspaceId]/providers/workspace-host-provider' import { CommandFadedList, CommandSearch, @@ -157,6 +159,8 @@ function SearchModalContent({ const router = useRouter() const { chatEnabled } = useDeploymentShape() const workspaceId = params.workspaceId as string + const host = useWorkspaceHostContext() + const integrationsHref = workspaceIntegrationsPath(workspaceId, host) const currentWorkflowId = params.workflowId as string | undefined const inputRef = useRef(null) const listRef = useRef(null) @@ -249,7 +253,7 @@ function SearchModalContent({ id: 'integrations', name: 'Integrations', icon: Integration, - href: `/workspace/${workspaceId}/integrations`, + href: integrationsHref, hidden: permissionConfig.hideIntegrationsTab && !requestableFeatureKeys.has('hideIntegrationsTab'), @@ -318,6 +322,7 @@ function SearchModalContent({ permissionConfig.hideTablesTab, permissionConfig.hideFilesTab, permissionConfig.hideIntegrationsTab, + integrationsHref, requestableFeatureKeys, ] ) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx index c022fceb707..e12b5676df1 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx @@ -55,6 +55,7 @@ import { buildFolderTree, getFolderPathNames } from '@/lib/folders/tree' import { DOCS_URL, SLACK_COMMUNITY_URL } from '@/lib/help-links' import { isIntegrationUsable } from '@/lib/integrations/access' import { SIM_RESOURCES_DRAG_TYPE } from '@/lib/mothership/resource-types' +import { workspaceIntegrationsPath } from '@/lib/navigation/paths' import { captureEvent } from '@/lib/posthog/client' import { LOGO_ACCEPT_ATTRIBUTE } from '@/lib/uploads/client/logo-file' import { useSidebarChrome } from '@/app/workspace/[workspaceId]/components/workspace-chrome' @@ -415,6 +416,7 @@ export const Sidebar = memo(function Sidebar({ organizationHref, desktopExecutor const { data: sessionData, isPending: sessionLoading } = useSession() const { data: profile } = useUserProfile() const hostContext = useWorkspaceHostContext() + const integrationsHref = workspaceIntegrationsPath(workspaceId, hostContext) const { workspace: routeWorkspace } = hostContext const { hosted, chatEnabled } = useDeploymentShape() const { canAdmin, canEdit, isLoading: permissionsLoading } = useUserPermissionsContext() @@ -798,7 +800,7 @@ export const Sidebar = memo(function Sidebar({ organizationHref, desktopExecutor id: 'integrations', label: 'Integrations', icon: Integration, - href: `/workspace/${workspaceId}/integrations`, + href: integrationsHref, additionalActivePaths: [`/workspace/${workspaceId}/skills`], hidden: permissionConfig.hideIntegrationsTab && @@ -812,6 +814,7 @@ export const Sidebar = memo(function Sidebar({ organizationHref, desktopExecutor canEdit, permissionsLoading, permissionConfig.hideIntegrationsTab, + integrationsHref, permissionConfig.hideCopilot, permissionConfig.hideFilesTab, requestableFeatureKeys, diff --git a/apps/sim/lib/navigation/paths.ts b/apps/sim/lib/navigation/paths.ts index 6878d4bf9f6..65e4895d3e0 100644 --- a/apps/sim/lib/navigation/paths.ts +++ b/apps/sim/lib/navigation/paths.ts @@ -59,6 +59,21 @@ export function organizationRoutes(organizationId: string) { } as const } +interface WorkspaceIntegrationContext { + hostOrganizationId: string | null + features?: { organizationSearch?: boolean } +} + +/** Organization members use the center; standalone workspaces retain their credential routes. */ +export function workspaceIntegrationsPath( + workspaceId: string, + context: WorkspaceIntegrationContext | null +) { + return context?.features?.organizationSearch && context.hostOrganizationId + ? organizationRoutes(context.hostOrganizationId).integrations + : `/workspace/${workspaceId}/integrations` +} + /** Where `sim//` resolves in this app: the address a member types, kept verbatim. */ export function simLinkPath(...segments: string[]): string { return ['/sim', ...segments.map(encodeURIComponent)].join('/') diff --git a/apps/sim/package.json b/apps/sim/package.json index 4deeddf2be1..e122a0199bd 100644 --- a/apps/sim/package.json +++ b/apps/sim/package.json @@ -9,13 +9,14 @@ }, "scripts": { "dev": "bun run build:cli-runtime && bun run dev:cache:cap && next dev --port 3000", - "dev:platform": "cd platform && next dev --webpack --port 3100", - "build:platform": "cd platform && next build --webpack", + "build:workspace-preview": "vite build --config workspace-preview/vite.config.ts", + "dev:platform": "bun run build:workspace-preview && cd platform && next dev --webpack --port 3125", + "build:platform": "bun run build:workspace-preview && cd platform && next build --webpack", "type-check:platform": "cd platform && next typegen && tsc --noEmit", "test:platform:desktop": "playwright test --config platform/checks/desktop/playwright.config.ts", "test:platform:e2e": "playwright test --config platform/checks/playwright.config.ts", "test:platform:live": "playwright test --config platform/checks/live/playwright.config.ts", - "start:platform": "cd platform && next start --port 3100", + "start:platform": "cd platform && next start --port 3125", "dev:capped": "bun run build:cli-runtime && bun run dev:cache:cap && NODE_OPTIONS='--max-old-space-size=4096' next dev --port 3000", "dev:cache:cap": "bun run ../../scripts/prune-turbopack-cache.ts", "dev:clean": "rm -rf .next/dev/cache", @@ -351,6 +352,7 @@ "tinyrainbow": "3.2.0", "tw-animate-css": "^1.4.0", "typescript": "^7.0.2", + "vite": "8.3.5", "vitest": "^5.0.4" } } diff --git a/apps/sim/platform/AGENTS.md b/apps/sim/platform/AGENTS.md index 31246d41828..1e051670ae9 100644 --- a/apps/sim/platform/AGENTS.md +++ b/apps/sim/platform/AGENTS.md @@ -17,27 +17,37 @@ Search existing EMCN and route owners first. For existing designs, follow Shared ## Code-first product design -The running product and the latest approved direction govern implementation. Use Paper to identify anatomy, semantic roles and interaction states. A new code-first variant must have a reachable product consumer; record Paper keys only if an actual counterpart exists. Preserve historical design evidence without constructing a fake counterpart. When an external interface is a visual reference, translate its proportions and behavior into Sim’s existing tokens and component names. +The running product and the latest approved direction govern implementation. Use Paper to identify anatomy, semantic roles and interaction states. A new shared visual variant requires at least two independent real consumers. Keep a one-feature composition local and compose existing primitives; do not add a variant for a label, icon, data value or responsive arrangement. Remove unused variants. Record Paper keys only if an actual counterpart exists. Preserve historical design evidence without constructing a fake counterpart. When an external interface is a visual reference, translate its proportions and behavior into Sim’s existing tokens and component names. -Keep routes thin. Colocate each feature component, its CSS Module and private helpers in `app/(shell)//_components//`. `(shell)/_components/` owns parts shared by its descendant routes (navigation, conversation composer, session). Generic controls live in `design-system/components//`; import the same implementation everywhere. Each CSS Module belongs to its component folder. Base UI adapters inside EMCN may share the named `*.shared.module.css` button and popup recipes. Product components compose the corresponding EMCN controls through props. The workbench may render product owners. Product code and its variant-use graph exclude workbench modules. No customer-facing development notices. Unimplemented external actions stay unavailable. +Keep routes thin. Colocate each feature component, its CSS Module and private helpers in `app/(shell)//_components//`, with `.tsx` as its entry. `(shell)/_components/` owns parts shared by its descendant routes (navigation, conversation composer, session). Generic controls live in `design-system/components//`; import the same implementation everywhere. Each CSS Module belongs to its component folder. Use a barrel for a family with several public exports, not a redundant wrapper around every file. Base UI adapters inside EMCN may share the named `*.shared.module.css` button and popup recipes. Product components compose the corresponding EMCN controls through props. Custom feature anatomy still uses the same typography, row geometry and semantic rhythm as an existing comparable surface. The workbench may render product owners. Product code and its variant-use graph exclude workbench modules. No customer-facing development notices. Unimplemented external actions stay unavailable. Use the semantic gaps in `design-system/styles/rhythm.css`. Title/detail stacks, sibling list items and adjacent actions each have a 2px role. The 8px row gap separates leading art from copy. Separate sibling rows with the list gap. Reserve lines for surface boundaries and data rules such as chart axes. Align a leading avatar or glyph with the first text line; center trailing metadata, badges and actions against the whole row. Align action ink with adjacent text by extending the hit area into the margin. Chat thinking, tool labels and prose share that text edge. Composer and message action groups use the same compact gap. Custom calendar, chart and summary rows use the same type and spacing. An icon that identifies a two-line notification aligns to the title; its trailing action group centers against the full row. Email thread counts and the notification trigger use the same centered inline label/count spacing. The notification trigger takes the compact secondary (Invite) button surface, and has no chevron. The tray applies control elevation once around its scroll viewport; stacked notices do not cast shadows on one another. An active popup stays above the tray, including at narrow widths. Labelled range pickers and inline ellipsis menus need accessible names, not redundant tooltips; keep tooltips on top-level icon actions. -Home and Chat share a centered, border-box reading column (maximum width and inline gutter). Align the resting Home greeting with the Home navigation row using the shell’s shared row origin **inside** the moving content. The unpadded viewport keeps the fade at the window edge. The web sidebar opener belongs to the shell, outside the scrolling page. It stays at the expanded toolbar’s 12px top inset as the center content scrolls, including on Home and in the drawer layout. Keep the button reachable without covering page actions. Electron keeps both controls in its native titlebar. Keep the same shell inset immediately above and below the footer. Major dashboard/sidebar sections are independently spaced at 24px. Responsive layouts can wrap, collapse or split panels; button height, padding and type stay stable. Composer draft and placeholder keep the same type role across widths and themes; browser zoom and user text scaling still apply. +Every Platform reading page uses `PageColumn` for the same centered, border-box column, with Home’s 1296px content maximum and fluid 16–48px gutters. `PageOverflow` extends surfaces 8px around the text edge. Home, Chat, Search, Settings, integration details, resource directories and people pages share these edges. Embedded editors and split preview canvases retain their available working space. PageColumn owns geometry only and preserves Home’s legacy theme isolation. Home and Integrations use the shared page-heading inset inside the moving content. The shell separately reserves clearance for its sidebar opener or native titlebar; responsive layout does not change either page’s own heading inset. The unpadded viewport keeps the fade at the window edge. The web sidebar opener belongs to the shell, outside the scrolling page. It stays at the expanded toolbar’s 12px top inset as the center content scrolls, including on Home and in the drawer layout. Keep the button reachable without covering page actions. Electron keeps both controls in its native titlebar. Keep the same shell inset immediately above and below the footer. Major dashboard/sidebar sections are independently spaced at 24px. Home uses the compact 3px heading gap from greeting to description, Recents to links and activity title to total. Use the 12px content gap from the Sim heading row to its input. Home widget content uses the 16px gap measured from the activity total to the chart, and from the attention, Email and Calendar labels to their first content. Header and calendar controls extend around their label line without adding height to these gaps. The ordinary 12px section gap still serves other compositions. Home has two inline alignment edges: headings and leading content share the reading-column text edge; the composer, row hover surfaces and outer control bounds extend by the shared 8px row inset. Keep Sim and the Build/Search toggle on the text edges, and align the composer and its loading placeholder with the leftmost recent and Email row on the left and the attention row and menu controls on the right. Measure the 24px dashboard gap between component boxes, including their inline hover overhang. Chart and widget boxes own that inset; the column layout allows for both insets so neighboring surfaces remain 24px apart. Vertical widget gaps use the same 24px as the description-to-Sim gap. Responsive layouts can wrap, collapse or split panels; button height, padding and type stay stable. Composer draft and placeholder keep the same type role across widths and themes; browser zoom and user text scaling still apply. -Use `ControlRow` for the sidebar, identity controls, panel tabs, recent links, wrapping workspace choices and horizontal destination links. The row owns its 30px height, 8px inset and the placement and hover of a 24px action, including a leading remove button that replaces an existing glyph. Workspace names, the Workspaces launcher and its tab title are text-only. A recent without a glyph keeps its remove action at the end. Keep the link, tab or button semantics on the rendered element, with sibling actions outside it. Whole section headers reveal their chevron on hover; identity controls need none. Ordinary buttons, icon triggers and rows share the sidebar Home radius (`--ui-control-radius`). Use `Button` / `IconButton` for actions and Base UI triggers, passing icons and labels through their slots. The composer toolbar owns its round actions. Switches and segmented controls keep their pill shape. Clicking the selected half of a two-choice segment switches to the other half. Native keyboard choice navigation still selects its destination. +Use `ControlRow` for the sidebar, identity controls, panel tabs, recent links, wrapping workspace choices and horizontal destination links. The row owns its 30px height, 8px inset and the placement and hover of a 24px action, including a leading remove button that replaces an existing glyph. Workspace names, the Workspaces launcher and its tab title are text-only. A recent without a glyph keeps its remove action at the end. A pinned workspace or chat shows its pin in the row action slot. Hover or opening the menu reveals the ellipsis in place. On touch, tapping the resting pin opens the same menu. Sort pinned rows first, applying the section’s chosen order within each group. Keep the link, tab or button semantics on the rendered element, with sibling actions outside it. Whole section headers reveal their chevron on hover; identity controls need none. Ordinary buttons, icon triggers and rows share the sidebar Home radius (`--ui-control-radius`). Use `Button` / `IconButton` for actions and Base UI triggers, passing icons and labels through their slots. The composer toolbar owns its round actions. Switches and segmented controls keep their pill shape. Clicking the selected half of a two-choice segment switches to the other half. Native keyboard choice navigation still selects its destination. -Use `ActivityRow` for a first-line glyph or avatar, title/detail and centered trailing metadata. Use the chart, calendar, composer or attachment owner when those parts have a different interaction. See the [component ownership map](docs/component-ownership.md) before adding another family. +Use `Stack` for compact people or integration artwork: show at most three items, a passive `+N` overflow count, and each item’s name on hover. Counts remain intrinsic-width and use compact notation from one thousand; never replace them with an ellipsis action. Each hovered item shifts left by the shared inset without moving its siblings. Use the shared motion duration and respect reduced motion. `AvatarGroup` supplies people to the same stack for integration usage and calendar attendees. -Measure the actual scroll viewport with `ScrollArea`, or attach `useScrollEdges` directly to a native or Base UI scroll owner. Use the shared 16px mask only on edges with more content, updating after scroll, resize, input, reset or filtering. Platform and Settings navigation hide the native scrollbar and keep their edge fades, keyboard, wheel and touch scrolling. Other scroll areas keep their existing scrollbar behavior. Insets establish resting layout; they cannot substitute for an edge fade. Home widgets and notification panels use `--ui-widget-max-size` as a ceiling, not a forced height. Keep the dashboard free to scroll and let each overflowing list or schedule scroll independently. Plain tooltips preview at most three lines; retain the complete accessible text. A structured people popover owns its content and uses the dropdown’s popup padding and row insets. +Use `ActivityRow` for a first-line glyph, avatar or artwork stack, title/detail and centered trailing metadata. Its trailing metadata is either text or people, never both. A navigating row ends at its chevron and has no sibling action or ellipsis. Put record actions in its detail; a passive row with independent controls uses ActivityBody and has no whole-row hover or click target. Use the chart, calendar, composer or attachment owner when those parts have a different interaction. See the [component ownership map](docs/component-ownership.md) before adding another family. -Form dialogs use `Modal` or `MultiStepModal`. Pass a title, content, action labels and handlers. A confirm handler that returns a promise holds the dialog open as pending, closes it on success, and keeps the draft with an inline message on failure; nothing dismisses it while pending. Returning `false` keeps it open at the dialog's next stage, and `onBack` turns the quiet footer action into a step back. The shared borderless surface positions its title at the logical start and supplies one quiet Cancel/Back. Command search and previews use the same shadow and dismissal layer. Form controls cannot construct or style a form header, footer or Cancel button. Opening a dialog or advancing its step focuses the first available field. Confirmation dialogs focus Cancel, and read-only previews focus Close. A nested dialog handles Escape one layer at a time. While a form is open, the global search shortcut leaves its draft and keyboard focus in place. Closing returns focus to the invoking control. Textareas scroll or grow within their owner and never show a manual resize handle. Recipient fields use the textarea’s height, draft typography and inset. Committed addresses use compact caption text and an always-visible neutral ×, like a panel tab. The remove button sits inside the tag beside its label; long addresses yield space to the button. Comma or Enter commits a draft, pasting a list commits its addresses, and Backspace from an empty draft removes the last tag. +Inputs and textareas share the neutral field frame, resting border and 8px text inset. Hover changes only the frame border; the fill remains unchanged. Focus alone preserves resting paint. Required placeholders describe the expected input without replacing the label. `viewOnly` supplies native read-only behavior and an always-visible shared copy action in the sidebar’s 24px trailing slot. Textareas place the same action at the top-right. Copy feedback remains visible for four seconds before returning to the copy action. Read-only fields show a not-allowed cursor without dimming, while their copy action remains interactive. TextArea defaultSize selects regular (76px), expanded (112px) or large (240px); skill instruction editors use large. Invite recipient fields share the regular textarea height and insets. Quiet `SearchField` is the explicit focus exception: its subtle underline starts at the typing edge, excluding the leading icon and trailing controls, and appears on hover or focus while the background remains unchanged. Counts inherit adjacent label metrics, use ordinary inline spacing and align on the text baseline. Disabled dropdowns show a not-allowed cursor. + +Toolbar choices use `ChoiceMenu`, the same compact quiet button and menu as Home's activity period. A toolbar action uses the existing quiet compact Button or IconButton. Page-level actions live in the control bar beneath the title and description; an action for a particular record belongs with that record. `Select` and `SelectField` provide field chrome for forms and settings; do not place a full-width field picker in a control bar or invent a toolbar variant to disguise it. Reuse the existing owner for the interaction and compare its actual trigger with Home before accepting it. + +Measure the actual scroll viewport with `ScrollArea`, or attach `useScrollEdges` directly to a native or Base UI scroll owner. Use the shared 16px mask only on edges with more content, updating after scroll, resize, input, reset or filtering. Platform and Settings navigation hide the native scrollbar and keep their edge fades, keyboard, wheel and touch scrolling. Other scroll areas keep their existing scrollbar behavior. Insets establish resting layout; they cannot substitute for an edge fade. Home list widgets and notification panels use `--ui-widget-max-size` as a ceiling, not a forced height. The Home activity chart grows with its content and has no nested scroll viewport. Keep the dashboard free to scroll and let each overflowing list or schedule scroll independently beneath its stationary heading. Shared ScrollArea viewports and the Home calendar suppress rubber-band overscroll at their boundaries. Plain tooltips preview at most three lines; retain the complete accessible text. A structured people popover owns its content and uses the dropdown’s popup padding and row insets. + +Form dialogs use `Modal` or `MultiStepModal`. Pass a title, content, action labels and handlers. A confirm handler that returns a promise holds the dialog open as pending, closes it on success, and keeps the draft with an inline message on failure; nothing dismisses it while pending. Returning `false` keeps it open at the dialog's next stage, and `onBack` turns the quiet footer action into a step back. The shared borderless surface positions its title at the logical start and supplies one quiet Cancel/Back. Command search and previews use the same shadow and dismissal layer. Form controls cannot construct or style a form header, footer or Cancel button. Opening a dialog or advancing its step focuses the first available field. Confirmation dialogs focus Cancel, and read-only previews focus Close. A nested dialog handles Escape one layer at a time. While a form is open, the global search shortcut leaves its draft and keyboard focus in place. Closing returns focus to the invoking control. Textareas scroll or grow within their owner and never show a manual resize handle. Modal content uses the shared field family: TextField, TextAreaField, SelectField, SecretField, RecipientField, FileField, DetailsField and ListField. Product dialogs provide values, labels and callbacks; they do not recreate field chrome. Full-width FileField centers its prompt without an upload glyph; its image kind supplies a compact preview and independent removal. DetailsField labels read-only information, and ListField owns grouped bullets and bounded scrolling. Use DataTable inside a dialog only for actual tabular data or selection, not static skill instructions. Recipient fields use the textarea’s height, draft typography and inset. Committed addresses use compact caption text and an always-visible neutral ×, like a panel tab. The remove button sits inside the tag beside its label; long addresses yield space to the button. Comma or Enter commits a draft, pasting a list commits its addresses, and Backspace from an empty draft removes the last tag. + +ContentTabs uses Base UI tab behavior with the same ControlRow lane, spacing and paint as DestinationTabs. DataTable composes those tabs, a caller-owned toolbar, native table semantics and shared checkboxes for controlled row/select-all operations. Passive table rows have no whole-row hover or click target; their controls remain independent. Consumers own filtering, sorting and mutation policy. + +MessageBubble owns the normal user-message surface and selectable integration ideas. Idea messages sit centered over full-width monochrome meeting artwork, with a translucent fill and at most two visible lines. Hover and press change the muted fill without adding a border. Selecting one appends the full prompt to the Home draft without submitting or discarding existing text or attachments. Use `useClipboard` / `CopyButton` for successful copy feedback and `useFeedbackExpiry` for temporary status. Copied indicators and transient notices dwell for four seconds; repeated events restart the timer. Pause an actionable notice during hover or focus and keep its action available in the stack afterward. Icon color changes use `--ui-motion-color` (120ms, zero under reduced motion). Do not put literal timers or CSS durations in product components. Opening a side-by-side panel keeps the chat title at the same inline and block position. A full-width panel takes over the reading surface until the reader returns to chat. Run disclosure controls thinking and tool activity. Assistant prose stays visible when activity collapses. Each chat message owns a continuous hover area from its content through its action row. Its timestamp and actions stay available when navigating by keyboard and on touch. Chat thinking, tool summaries and prose paragraphs share one 12px block gap. Expanded payloads have equal block padding; the final message keeps a section of clearance from the composer. Render pasted text, files and images with ControlRow and a sibling remove action. Tool summaries use one body-size label with quieter ink. A run divider, expanded tool rail and split-panel edge are structural rules with one shared hairline role. Ordinary list items remain separated by gap alone. Panel tabs, action controls and key values have their own semantics; the same CopyButton copies message text and either tool payload. Composer and jump-to-latest share their border, background and shadow; leave space between them. -Interface strokes have one non-scaling optical weight in CSS pixels. Size glyphs relative to the control’s type, capped at the shared icon size. Paint interface strokes with opaque semantic ink so overlapping segments do not darken; branded artwork and chart marks retain their own geometry. Neutral quiet controls gain the hover surface without changing ink. Destructive and primary confirmations change perceived lightness by the same amount within each theme; check rendered colors, not blend percentages. Destructive menu commands turn red on hover or keyboard highlight. A compact × dismisses a tab, recent, draft attachment or recipient with the ordinary quiet IconButton. Callers choose intent, never paint. A selectable menu row gains the same surface and promotes its glyph from secondary to primary ink on hover or active keyboard navigation. Selection, disabled and inverse controls have distinct semantics. Never change stroke thickness, spacing or radius on hover. Render interface glyphs through `Icon`. A unique data graphic may own accessible, labeled SVG and paint its own marks. Pointer focus alone adds no paint. +Interface strokes have one non-scaling optical weight in CSS pixels. Size glyphs relative to the control’s type, capped at the shared icon size. Paint interface strokes with opaque semantic ink so overlapping segments do not darken; branded artwork and chart marks retain their own geometry. Neutral quiet controls gain the hover surface without changing ink. Destructive and primary confirmations change perceived lightness by the same amount within each theme; check rendered colors, not blend percentages. Destructive menu commands turn red on hover or keyboard highlight. A compact × dismisses a tab, recent, draft attachment or recipient with the ordinary quiet IconButton. Callers choose intent, never paint. A selectable menu row gains the same surface and promotes its glyph from secondary to primary ink on hover or active keyboard navigation. Selection, disabled and inverse controls have distinct semantics. Never change stroke thickness, spacing or radius on hover. Render interface glyphs through `Icon`. A unique data graphic may own accessible, labeled SVG and paint its own marks. Pointer focus alone adds no paint, except the explicitly requested SearchField underline. Use [the shared Platform time formatters](shared/time.ts) for displayed ages, clocks, calendar ranges and chart dates. Keep raw instants or ISO calendar days in data. Relative ages use a single reference instant for the surface: now below one minute, whole minutes below an hour, hours below 24 hours, then `1d ago` until 48 hours. After that show an ordinal date, including the year when it differs. Preserve calendar days across time zones. Pass the viewer’s zone for a locally composed message. Fixture time is server-gated and fixed for reproducible rendering. A Home activity chart keeps one visibility setting and one period across modes: Build shows Tasks run and Search shows Searches. Both draw real dated daily counts; the axis, tooltip and Customize Home label must agree with the selected metric. @@ -45,8 +55,8 @@ Use [the shared Platform time formatters](shared/time.ts) for displayed ages, cl - A page supplies data, semantic slots, supported variants and callbacks. It owns no CSS, inline styles, Tailwind classes or visual overrides, including layout around a component. A composition with its own spacing is itself a recipe with an owner. Keep the smallest readable recipe that serves the real callers. Layout belongs to the composition. Changes to a child control’s appearance belong to that control’s owner, including when a parent selector could reach it. No `className`, `style`, utility pass-through, arbitrary JSX prop spread or per-screen colors/geometry. - Repeated data takes a typed array with stable item IDs. A specimen's row count is an example; render zero, two or more than the reference count without numbered props. Preserve genuinely distinct fixed slots. -- Keep the smallest API that describes the behavior. Use semantic HTML and import unstyled Base UI primitives inside EMCN for complex behavior. EMCN owns appearance. Base UI can own keyboard navigation, typeahead, focus management, dismissal and floating placement. Use named native controls for interactive masters and typed record arrays for repeated data. A static structural recipe does not provide those behaviors. A different label, icon, item count or viewport width does not create another variant. -- Pointer focus changes where typing, Tab and arrow keys go and adds no ring, outline, border change, shadow or background treatment. Keyboard focus is visible: `:focus-visible` may paint, as review on the running app settled. Keep the caret, validation, hover, selection, open state and the active row during list navigation distinct. Exercise Tab, Escape, focus restoration and typing in both themes. +- Keep the smallest API that describes the behavior. Prefer data, semantic slots and callbacks over combinations of visual flags. Do not add nested variant objects or independently tunable spacing, radius and color props. Use semantic HTML and import unstyled Base UI primitives inside EMCN for complex behavior. EMCN owns appearance. Base UI can own keyboard navigation, typeahead, focus management, dismissal and floating placement. Use named native controls for interactive masters and typed record arrays for repeated data. A static structural recipe does not provide those behaviors. A different label, icon, item count or viewport width does not create another variant. +- Pointer focus changes where typing, Tab and arrow keys go and adds no ring, outline, border change, shadow or background treatment, except SearchField’s shared hover/focus underline. Keyboard focus is visible: `:focus-visible` may paint, as review on the running app settled. Keep the caret, validation, hover, selection, open state and the active row during list navigation distinct. Exercise Tab, Escape, focus restoration and typing in both themes. - Every value picker, including workbench fields and modal fields, uses the shared dropdown surfaces and row recipes. Use Select for choices, Combobox for search, and Menu for commands. Use Base UI's portalled positioner for root and nested popups, inside the active theme and modal layer. Submenus open on hover and by keyboard, preserve pointer travel into the child, and flip or shift within the available space. The parent and child close one level at a time with Escape. - Keep tokens and composite recipes central. The Tailwind theme maps to semantic variables. An owner module may apply structural Tailwind utilities and registered theme roles; callers only use its public API. `PlatformSurface` selects the theme for the tree. Modules in an owner can express grid/flex, intrinsic sizing, focus, wrapping and overflow. Do not bake a screenshot into a component. At the default root size, match the Paper measurements retained by the current product direction. Use scalable type and spacing, bounded widths, flexible tracks, logical insets and natural height. Breakpoints follow a change in composition, not the number of Paper examples. Fixed stroke widths, icon geometry, touch targets and aligned data lanes are deliberate. Test intermediate widths, long content and zoom. - Preserve theme roles, Geist and Geist Mono recipes, 16px directional overflow fades, shell/floating glass and quiet reading surfaces. Compare real semantic parts and nested controls, not just matching names. A gallery or a token declaration is not evidence that a variant is needed. Remove unused variants, their styles and documentation. When an explicit product direction revises Paper, implement it in the owning component and verify it on the real route. @@ -93,6 +103,12 @@ Markup differs between the trees; nothing else should. `check:platform-convergen 5. When the trees disagree on what a viewer sees, leave that one function in each tree and raise it. Choosing is a product decision, not a refactor. 6. Run `bun run scripts/check-platform-convergence.ts --update` and `bun run scripts/check-monorepo-boundaries.ts --update` to record the shrink. +## Workspace chrome + +Workspace navigation shares the organization sidebar header, profile, Help and toolbar. Switch only the navigation body. Use the organization Home row position for Back, followed by New chat, Integrations and Skills. Integrations opens the same organization integration center; no workspace-owned management page exists. Place the workspace’s Chats section before the section labeled Workspace, then show Workflows. A new chat and its conversation stay in the selected workspace. Use the composer’s slash for Skills and each skill. Keep workspace directories on the shared Platform directory and control recipes. The [resource and workflow preview](docs/workspace-ui.md) runs the existing workspace presentation in its own document inside the right pane. + +Workspaces and chats partition pinned items first, then apply the section’s chosen sort within both partitions. The pin occupies the row’s trailing action slot and gives way to the ellipsis on hover or when its menu opens. Use the Home review book glyph for documentation throughout the Platform. Help excludes Changelog. + ## Product UI and desktop - Keep a draft or interface edit in memory for the lifetime its user expects. A chat draft belongs to its chat; profile edits and inbox read marks belong to the product session. Navigating away and back must preserve them. A new fixture scenario starts a fresh session. Popups and cancelled edits stay local to the control. @@ -106,6 +122,10 @@ Treat an explicit correction as a change at the narrowest owner that explains it When removing or consolidating a component, trace its real callers. Migrate their data, keyboard and pointer behavior before retiring the old exports, styles, variants and unused fixture or workbench references. Prefer a narrow audit for a rule that code can reliably detect, and a rendered regression for behavior that has failed in practice. Demonstrate the failure on the old implementation. Preserve dated design evidence separately from current product decisions. +## Shared local review + +Keep one user-facing Platform preview at `http://localhost:3125`, served with `dev:platform` from the agreed review checkout. The preview follows that checkout’s files; commits on other branches or worktrees do not appear automatically. At session start, inspect the listener’s working directory, branch and uncommitted changes, then reuse it. Do not start a second review server or silently choose another port. Parallel tasks may use separate branches for review, but their changes must be reconciled into the shared preview checkout before handing the UI back. Preserve concurrent work and identify the preview owner in a checkpoint. Temporary automated-test servers are isolated from review and must be stopped afterward. + ## Verification Run `bun run check:platform-design`, build the isolated Next app, and run its browser checks (commands in README.md). Live data has its own suite in `checks/live/`, which runs against a real app and database. Record each organization capability's status and proving check in the [parity ledger](docs/org-parity.md). Review actual Paper and browser rendering, wrapping, interactive states, keyboard and focus, both themes, content overflow and intermediate widths. Check disabled fixture output independently. A passing static audit is not a visual comparison. Record the affected keys, readback, rendered verdict, uncertainty and next action in the task checkpoint. Run the repository gates before shipping. diff --git a/apps/sim/platform/README.md b/apps/sim/platform/README.md index 0c5af6cd966..6fdef023c8d 100644 --- a/apps/sim/platform/README.md +++ b/apps/sim/platform/README.md @@ -4,20 +4,26 @@ The new Platform starts here as a standalone Next.js App Router project inside ` To start or resume work in a new Sim chat, say “Use `$implement-platform` to [add, change, consolidate, or remove the UI] on [route].” The [implementation skill](../../../.agents/skills/implement-platform/SKILL.md) reads the [contract](AGENTS.md) and [ownership map](docs/component-ownership.md) for the checkout containing that route. Name the Paper section when implementing an existing design. For a code-first change, describe the desired behavior, where it should apply, and what you want to see on the live page. -From the repository root: +The shared review URL is **http://localhost:3125**. Use the existing listener and its checkout when another task is active. `next dev` reloads changes from that checkout; a different worktree is a different copy of the code. Reconcile parallel changes there before review. Do not run another review server on a new port. Temporary test servers shut down after verification. + +From the agreed review checkout’s repository root: ```sh bun run --cwd apps/sim dev:platform SIM_PLATFORM_FIXTURES=1 bun run --cwd apps/sim dev:platform ``` -Open [Home](http://localhost:3100/home), [Chat](http://localhost:3100/chat/weekly-review?state=full), [Settings](http://localhost:3100/settings), [Integrations](http://localhost:3100/integrations?state=full), or the separate [component workbench](http://localhost:3100/workbench). With the flag on, `?state=empty|full|stress` selects the fixture on any of them. `?theme=light|dark|system` selects the product appearance. Product navigation preserves these choices; nothing writes examples to storage. The URL cannot activate the server flag. Old catalog links with a recipe still open the workbench. +Open [Home](http://localhost:3125/home), [Chat](http://localhost:3125/chat/weekly-review?state=full), [Settings](http://localhost:3125/settings), [Integrations](http://localhost:3125/integrations?state=full), or the separate [component workbench](http://localhost:3125/workbench). With the flag on, `?state=empty|full|stress` selects the fixture on any of them. `?theme=light|dark|system` selects the product appearance. Product navigation preserves these choices; nothing writes examples to storage. The URL cannot activate the server flag. Old catalog links with a recipe still open the workbench. + +Settings uses the same organization catalog and section components in fixture and live routes. [General](http://localhost:3125/settings/general?state=full) includes Privacy and Account details; [Sources](http://localhost:3125/settings/integrations?state=full) is the administrator's Search source inventory. The [integration center](docs/integration-center.md) is the member-facing management surface for Apps, Skills, MCPs and Credentials. Settings examples are loaded only by the server's fixture flag. Their edits belong to the browser session; no account, credential, organization policy or subscription is changed by preview interactions. -Settings uses the same organization catalog and section components in fixture and live routes. [General](http://localhost:3100/settings/general?state=full) includes Privacy and Account details; [Sources](http://localhost:3100/settings/integrations?state=full) is the administrator's Search source inventory. The personal Integrations page is separate. Settings examples are loaded only by the server's fixture flag. Their edits belong to the browser session; no account, credential, organization policy or subscription is changed by preview interactions. +Integration fixtures cover the full workspace app catalog, multiple credentials, OAuth and provider-specific service-account forms, access management, suggested skills and templates. App-level actions use the control bar; credential rows open their own details. Family credentials share a canonical ID across apps. New enrollment and sharing remain UI-only, with secrets confined to the open form. The existing live Search adapter keeps its own authorized inventory and actions until the broader credential seam is connected. -Settings fixtures default to Enterprise at every density. The [Team subscription](http://localhost:3100/settings/billing?state=stress&plan=team) and [Free subscription](http://localhost:3100/settings/billing?state=empty&plan=free) previews apply the selected plan to Billing, Members and the available Settings sections. The [Knowledge graphs preview](http://localhost:3100/settings/knowledge-graphs?state=stress) uses the stress viewer’s platform-admin role, benchmark access and enabled superuser mode. +Settings fixtures default to Enterprise at every density. The [Team subscription](http://localhost:3125/settings/billing?state=stress&plan=team) and [Free subscription](http://localhost:3125/settings/billing?state=empty&plan=free) previews apply the selected plan to Billing, Members and the available Settings sections. The [Knowledge graphs preview](http://localhost:3125/settings/knowledge-graphs?state=stress) uses the stress viewer’s platform-admin role, benchmark access and enabled superuser mode. -The product routes live under `app/(shell)/`. Their shared App Router layout owns the shell, sidebar scroll viewport, interface preferences and chat edits. Home, Chat, Settings and Integrations pages render their own bodies inside that layout. Navigating between them keeps the sidebar reading position, including when returning from Settings. The shell selects its gated fixture and appearance from the current URL. Each route owns its components and CSS Modules in a private `_components//` folder: the [Home chart](app/%28shell%29/home/_components/activity-chart/activity-chart.tsx) sits beside its styles; the [dialog](design-system/components/modal/modal.tsx) belongs to EMCN. Shared shell UI lives in `app/(shell)/_components/`. [Rhythm tokens](design-system/styles/rhythm.css) and the [ownership and interaction contract](AGENTS.md) apply throughout the product. The [Paper state inventory](docs/core-chat-states.md) enumerates the source rows in scope. The [organization integrations guide](docs/organization-integrations.md) maps the member inventory, credential actions and tool permissions to their Phase 3 operations. Workspace rows and chat panel workspace choices open the selected workspace’s Integrations tab in fixture mode. The workspace keeps the Platform sidebar header and footer and replaces its navigation with Back, Integrations, Skills, workspace resources and workflows. See [workspace UI](docs/workspace-ui.md) for routes, preview scope and verification. Wiki is deferred. +The product routes live under `app/(shell)/`. Their shared App Router layout owns the shell, sidebar scroll viewport, interface preferences and chat edits. Home, Chat, Settings and Integrations pages render their own bodies inside that layout. Navigating between them keeps the sidebar reading position, including when returning from Settings. The shell selects its gated fixture and appearance from the current URL. Each route owns its components and CSS Modules in a private `_components//` folder: the [Home chart](app/%28shell%29/home/_components/activity-chart/activity-chart.tsx) sits beside its styles; the [dialog](design-system/components/modal/modal.tsx) belongs to EMCN. Shared shell UI lives in `app/(shell)/_components/`. [Rhythm tokens](design-system/styles/rhythm.css) and the [ownership and interaction contract](AGENTS.md) apply throughout the product. The [Paper state inventory](docs/core-chat-states.md) enumerates the source rows in scope. The [organization integrations guide](docs/organization-integrations.md) maps the member inventory, credential actions and tool permissions to their data operations. The integration center lives at the organization root; Home and eligible workspace links open that same destination. Standalone workspaces and viewers without organization Search access retain their existing credential route. Skills, MCPs and new sharing/editor interactions currently use only server-gated fixtures. Paper workspace tabs and the control bar inform the center's anatomy. Wiki is deferred. + +Workspace rows and chat panel workspace choices open the selected workspace’s New chat tab in fixture mode. The workspace keeps the Platform sidebar header and footer and replaces its navigation body with Back, New chat, Integrations, Skills, workspace chats, resources and workflows. Integrations opens the organization center. See [workspace UI](docs/workspace-ui.md) for the embedded resource preview and verification. ## Live data and the zone @@ -48,26 +54,24 @@ Sim Desktop loads **the same routes and styles** in its Electron window. [The sh Start the Platform above, then launch the existing desktop shell using a disposable profile: ```sh -SIM_DESKTOP_ORIGIN=http://127.0.0.1:3100 SIM_DESKTOP_USER_DATA="$(mktemp -d)" bun run --cwd apps/desktop dev +SIM_DESKTOP_ORIGIN=http://localhost:3125 SIM_DESKTOP_USER_DATA="$(mktemp -d)" bun run --cwd apps/desktop dev ``` ## Using EMCN -Select a theme once with `PlatformSurface`. Editors, menus, selections, links and dialogs own their keyboard behavior. `MetricGroup` and `DataTable` accept records of any length. Import controls and compositions from `@/platform/design-system/components`. Provide content and named states, never styles. For a structural master such as an Issue group, supply its Paper key and named slots. The key determines the allowed slot names and the compiler enforces missing slots. A nested button slot accepts the semantic button, not an image of it. The Paper catalog in the workbench resolves the actual example. +Select a theme once with `PlatformSurface`. Editors, menus, selections, links and dialogs own their keyboard behavior. `MetricGroup` and `DataTable` accept records of any length. Import controls and compositions from `@/platform/design-system/components`. Provide content and named states, never styles. Unique feature compositions live beside their route and use these controls. Paper structural recipes remain workbench specimens and do not substitute for interactive product components. ```tsx -import { ComponentRecipe } from '@/platform/design-system/catalog/component-recipe' import { Button } from '@/platform/design-system/components' - ``` Shared and Platform still own different recipes; `catalog/evidence.json` preserves that ownership and a path from the Base to a Platform product use. Registered states without a Platform use, canvas organizers and the other products are outside this library. Keep feature-specific compositions in their feature owner. Real data adapters live in `lib/data/`. Workspace lists, sidebar items, recents and attachment batches accept typed arrays with stable item IDs. Use the named components for these collection masters: a Paper specimen's item count is sample data. An empty array renders zero items, and adding items preserves their registered row/card appearance. Each scroll viewport determines fades and controls from its actual contents. Chart labels, marks, inspected values and legends come from the same series. Supply raw numeric values and their units to data tables. The column schema determines alignment and which columns remain visible as the table narrows. -The full fixture includes twenty sidebar workspaces, sixteen conversations, a twelve-message inbox and a calendar that scrolls from midnight to midnight even on empty days. Home and Chat share a reading width. The resting Home greeting aligns with the sidebar Home row. The shell uses one 12px inset above the organization and on both sides of the profile row. The Home content scrolls through its viewport edge fade. The shell keeps the collapsed sidebar button aligned with the expanded toolbar even as the page scrolls. Platform and Settings navigation scroll independently, with edge fades instead of a visible scrollbar. Calendar events use the muted surface. An event with a provider URL opens in a new tab. The attendee control opens its own people popover. Now appears when the calendar scrolls away from its resting position or another day is selected. It restores today’s schedule and centers the current time. Scrolling back to that position also hides Now. +The full fixture includes twenty sidebar workspaces, sixteen conversations, a twelve-message inbox and a calendar that scrolls from midnight to midnight even on empty days. All Platform reading pages share PageColumn width and gutters; PageOverflow supplies the common 8px surface overhang. Editor and preview canvases retain their working area. Home and Integrations share the same heading inset; the shell reserves sidebar opener and native titlebar clearance separately. The shell uses one 12px inset above the organization and on both sides of the profile row. The Home content scrolls through its viewport edge fade. The shell keeps the collapsed sidebar button aligned with the expanded toolbar even as the page scrolls. Platform and Settings navigation scroll independently, with edge fades instead of a visible scrollbar. Calendar events use the muted surface. An event with a provider URL opens in a new tab. Attendees and integration usage share Stack: up to three photos with names on hover and a passive +N count, compacted for large totals. Now appears when the calendar scrolls away from its resting position or another day is selected. It restores today’s schedule and centers the current time. Scrolling back to that position also hides Now. Start with the [component ownership map](docs/component-ownership.md) when choosing a primitive. Sidebar, recents and panel tabs use one row; form and command dialogs use one borderless surface. Copy confirmations and transient notifications last four seconds. The notification count uses the compact Invite button. Expanded notices share one subtle shadow around the list. A new notice pauses its expiry on hover or keyboard focus. Icon color transitions take 120ms and respect reduced motion. The [design contract](AGENTS.md) defines these rules. @@ -79,7 +83,7 @@ Import [Platform time formatting](lib/time.ts) for dates, relative ages and cloc ### Keyboard and dropdown behavior -Focus places the caret or keyboard target without changing the control’s border, outline, background or shadow. Selection, validation and the highlighted list item retain their Paper treatments. Use the same EMCN value picker and floating surface on the workbench and in a modal. Select handles a fixed set of values, a searchable combobox supports longer lists, and an action menu supports commands and nested menus. The Help menu in the workbench exercises the cascade. +Focus places the caret or keyboard target without changing ordinary field paint. SearchField has one explicit exception: its underline appears on hover or focus. Keyboard focus remains visible on actions. Selection, validation and the highlighted list item retain their Paper treatments. Use the same EMCN value picker and floating surface on the workbench and in a modal. Select handles a fixed set of values, a searchable combobox supports longer lists, and an action menu supports commands and nested menus. The Help menu in the workbench exercises the cascade. Import the styled EMCN owner from product UI. Its Base UI primitive handles pointer and keyboard interaction, dismissal and collision placement. Nested menus support hovering into the child, arrow-key navigation, and Escape back to the parent. The tests also check form submission, dialog-local placement and focus restoration. [Base UI Select](https://base-ui.com/react/components/select) · [Combobox](https://base-ui.com/react/components/combobox) · [Menu](https://base-ui.com/react/components/menu) @@ -90,6 +94,7 @@ The [Platform design contract](AGENTS.md) is authoritative. Use the repository's - **Styles:** obtain Shared Light `get_tokens` (the token array) and `get_computed_styles` for the type masters (the node-to-style map); confirm theme file hashes match before assuming identical vocabulary. Run `bun scripts/sync-platform-foundations.ts `, or append `--check` to compare without writing. This preserves aliases and converts scalable Paper px to rem at the 16px reference scale. Roles and material composites live in `design-system/styles`. The Tailwind entry scans only component owners; product pages do not receive a styling API. - **Structure:** capture current Shared/Platform master JSX locally with node IDs for the compiler's anatomy check in `/.jsx` and reconcile the registry and recipe contracts. Run `bun scripts/sync-platform-recipes.ts ` from the repository root. Review changed keys, token aliases, CSS and nested slot geometry in the rendered workbench. Do not edit generated selectors to fix a product use. - **Assets:** retain canonical glyph geometry and brand paints. Media belongs in named slots; do not fetch private Paper artwork at runtime. Fixture content is removable from `fixtures/`. Remove its loader, preview calls and synthetic data together when real resource reads arrive. +- **Integration catalog:** run `bun scripts/sync-platform-integration-metadata.ts` after changing public provider metadata, connection definitions, skills, templates or registry icons. The generated server-only catalog supplies selected detail content; the directory receives compact summaries. `bun run check:platform-integration-metadata` detects drift. Runtime product code never imports the executable block/tool registry. ## Verification @@ -108,8 +113,15 @@ The Platform type check generates Next.js route types and checks the isolated pr `bun run --cwd apps/sim test:platform:live` runs `checks/live/` against an app and database you have started, using `PLATFORM_LIVE_ORIGIN`, `PLATFORM_LIVE_EMAIL`, `PLATFORM_LIVE_PASSWORD` and `PLATFORM_LIVE_ORGANIZATION_NAME`. The member needs build access: anyone else lands on a legacy route. Supply `PLATFORM_LIVE_REPORT_PATH` to save its JSON report. -The browser suite starts separate production servers with fixtures enabled and disabled. Supply `PLATFORM_REPORT_PATH` to save JSON, failure screenshots and traces. Next uses webpack for this nested app: the installed Turbopack also discovers the parent Sim project's proxy and instrumentation. Watch for that boundary when upgrading Next. +The default browser suite starts production servers with fixtures enabled and disabled. For the already-running shared preview, use the explicit existing-server mode; it starts no server and leaves the preview process unchanged: + +```sh +PLATFORM_E2E_ORIGIN=http://localhost:3125 PLATFORM_REPORT_PATH= bun run --cwd apps/sim test:platform:e2e +PLATFORM_E2E_ORIGIN=http://localhost:3125 PLATFORM_DESKTOP_REPORT_PATH= bun run --cwd apps/sim test:platform:desktop +``` + +That local mode runs fixture-enabled browser and Electron coverage. It excludes only `fixtures-off.spec.ts`, whose server gate requires a fixture-disabled process. The default production suite retains those checks; complete them on the exact final commit before shipping. `?state=empty` does not turn fixtures off. Supply `PLATFORM_REPORT_PATH` to save JSON, failure screenshots and traces. Next uses webpack for this nested app: the installed Turbopack also discovers the parent Sim project's proxy and instrumentation. Watch for that boundary when upgrading Next. -The static check covers ownership, styling props, evidence, tokens, variant use and dependency direction. If it flags a variant used on a real route, verify the call path and add that case to the checker. A workbench example cannot establish product usage. Use the rendered route to verify visual alignment. Compare both themes, keyboard and pointer states, actual overflow, text scaling and widths between approved artboards. Build the standalone app before running the browser suite to test the current sources. +The static check covers ownership, styling props, component folders, evidence, tokens, unused variants and dependency direction. It rejects a route CSS Module directly under `_components/`; put it with its component. If it flags a variant used on a real route, verify the call path and add that case to the checker. Review new shared visual variants against at least two independent consumers; semantic independence is a design review, not a count of repeated JSX rows. A workbench example cannot establish product usage. Use the rendered route to verify visual alignment. Compare both themes, keyboard and pointer states, actual overflow, text scaling and widths between approved artboards. Build the standalone app before running the browser suite to test the current sources. References: [Next project structure](https://nextjs.org/docs/app/getting-started/project-structure), [Next styling](https://nextjs.org/docs/app/getting-started/css), [Tailwind theme variables](https://tailwindcss.com/docs/theme), [shadcn/ui composition](https://ui.shadcn.com/docs), [Playwright assertions](https://playwright.dev/docs/test-assertions). diff --git a/apps/sim/platform/app/(shell)/_components/chat/cards/assistant-review.tsx b/apps/sim/platform/app/(shell)/_components/chat/cards/assistant-review.tsx index 4038fe7a946..a57d702b76d 100644 --- a/apps/sim/platform/app/(shell)/_components/chat/cards/assistant-review.tsx +++ b/apps/sim/platform/app/(shell)/_components/chat/cards/assistant-review.tsx @@ -56,12 +56,14 @@ function ActionCard({ {editing && field.editable ? ( field.multiline ? (