Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .agents/skills/implement-platform/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ 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.
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/<component>/<component>.tsx` and `<component>.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. Provide the working localhost route for visual review.
6. **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.
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -17,9 +19,13 @@ export function IntegrationTabsHeader({
workspaceId,
rightSlot,
}: IntegrationTabsHeaderProps) {
const host = useWorkspaceHostContext()
return (
<div className={cn(PAGE_HEADER_BAR, 'gap-1')}>
<ChipLink href={`/workspace/${workspaceId}/integrations`} active={active === 'integrations'}>
<ChipLink
href={workspaceIntegrationsPath(workspaceId, host)}
active={active === 'integrations'}
>
Integrations
</ChipLink>
<ChipLink href={`/workspace/${workspaceId}/skills`} active={active === 'skills'}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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'
Expand All @@ -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,
Expand Down Expand Up @@ -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)

@cubic-dev-ai cubic-dev-ai Bot Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This redirect drops the workspace page’s category and search query state, so filtered or bookmarked integration URLs open the organization list unfiltered. Translate the supported state (at least search to the organization route’s q) before redirecting.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apps/sim/app/workspace/[workspaceId]/integrations/integrations.tsx, line 167:

<comment>This redirect drops the workspace page’s `category` and `search` query state, so filtered or bookmarked integration URLs open the organization list unfiltered. Translate the supported state (at least `search` to the organization route’s `q`) before redirecting.</comment>

<file context>
@@ -159,6 +161,10 @@ function ConnectedItem({ href, blockType, name, description, icon: Icon }: Conne
+  const host = useWorkspaceHostContext()
+  const workspaceId = host.workspace.id
+  const destination = workspaceIntegrationsPath(workspaceId, host)
+  if (destination !== `/workspace/${workspaceId}/integrations`) redirect(destination)
   return (
     <PermissionAccessBoundary configKey='hideIntegrationsTab'>
</file context>
Fix with cubic

Comment on lines +164 to +167

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Workspace credentials disappear from browsing

This redirects eligible members away from the workspace credential directory to a different list of member accounts. For example, the workspace page lists GitLab personal tokens, but the organization center treats GitLab as managed and supplies no accounts. Members lose the normal way to find and manage those credentials, and opening the old directory URL redirects them again.

Keep a workspace-credential entry reachable until the center supports these records and actions.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

return (
<PermissionAccessBoundary configKey='hideIntegrationsTab'>
<IntegrationsContent />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)

@cubic-dev-ai cubic-dev-ai Bot Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: This URL also needs the host-membership guard: a viewer can have a hosted workspace and organizationSearch enabled without belonging to the host organization, but workspaceIntegrationsPath then sends them to /o/<host>/integrations, whose layout denies non-members. Preserve the workspace integrations destination unless host.viewer.isHostOrganizationMember is true.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx, line 163:

<comment>This URL also needs the host-membership guard: a viewer can have a hosted workspace and `organizationSearch` enabled without belonging to the host organization, but `workspaceIntegrationsPath` then sends them to `/o/<host>/integrations`, whose layout denies non-members. Preserve the workspace integrations destination unless `host.viewer.isHostOrganizationMember` is true.</comment>

<file context>
@@ -157,6 +159,8 @@ function SearchModalContent({
   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<HTMLInputElement>(null)
</file context>
Suggested change
const integrationsHref = workspaceIntegrationsPath(workspaceId, host)
const integrationsHref = workspaceIntegrationsPath(
workspaceId,
host.viewer.isHostOrganizationMember ? host : null
)
Fix with cubic

const currentWorkflowId = params.workflowId as string | undefined
const inputRef = useRef<HTMLInputElement>(null)
const listRef = useRef<HTMLDivElement>(null)
Expand Down Expand Up @@ -249,7 +253,7 @@ function SearchModalContent({
id: 'integrations',
name: 'Integrations',
icon: Integration,
href: `/workspace/${workspaceId}/integrations`,
href: integrationsHref,
hidden:
permissionConfig.hideIntegrationsTab &&
!requestableFeatureKeys.has('hideIntegrationsTab'),
Expand Down Expand Up @@ -318,6 +322,7 @@ function SearchModalContent({
permissionConfig.hideTablesTab,
permissionConfig.hideFilesTab,
permissionConfig.hideIntegrationsTab,
integrationsHref,
requestableFeatureKeys,
]
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,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'
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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 &&
Expand All @@ -812,6 +814,7 @@ export const Sidebar = memo(function Sidebar({ organizationHref, desktopExecutor
canEdit,
permissionsLoading,
permissionConfig.hideIntegrationsTab,
integrationsHref,
permissionConfig.hideCopilot,
permissionConfig.hideFilesTab,
requestableFeatureKeys,
Expand Down
15 changes: 15 additions & 0 deletions apps/sim/lib/navigation/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/<name>/<values…>` resolves in this app: the address a member types, kept verbatim. */
export function simLinkPath(...segments: string[]): string {
return ['/sim', ...segments.map(encodeURIComponent)].join('/')
Expand Down
Loading
Loading