diff --git a/README.md b/README.md index b5d9b9c5252..7f51bd8f7ac 100644 --- a/README.md +++ b/README.md @@ -27,13 +27,12 @@ ### Self-hosted ```bash -npx simstudio +git clone https://github.com/simstudioai/sim.git && cd sim +bun run setup ``` Open [http://localhost:3000](http://localhost:3000) -Docker must be installed and running. Use `-p, --port ` to run Sim on a different port, or `--no-pull` to skip pulling the latest Docker images. -

The Sim platform — chat on the left, the visual workflow builder on the right

@@ -77,12 +76,6 @@ Docker must be installed and running. Use `-p, --port ` to run Sim on a di **Requirements:** [Bun](https://bun.sh/) and [Docker](https://www.docker.com/). -```bash -git clone https://github.com/simstudioai/sim.git && cd sim -bun install -bun run setup -``` - `bun run setup` is an interactive wizard: it provisions the database, generates secrets, writes your `.env` files, connects a Chat API key, and starts Sim the way you choose: - **Local dev** — run from source to contribute or hack on Sim @@ -110,7 +103,7 @@ Sim also supports local models via [Ollama](https://ollama.ai) and [vLLM](https: ## Chat API Keys -Chat is a Sim-managed service. `bun run setup` connects a Chat API key for you — sign in when it opens your browser and the key is stored automatically. To view, create, or revoke keys later, go to [sim.ai/account/settings/chat-keys](https://sim.ai/account/settings/chat-keys). +Chat is a Sim-managed service. `bun run setup` connects a Chat API key for you — sign in when it opens your browser and the key is stored automatically. To view, create, or revoke keys later, go to [sim.ai/selfhost/settings/chat-keys](https://sim.ai/selfhost/settings/chat-keys). ## Environment Variables diff --git a/apps/sim/app/account/settings/chat-keys/page.tsx b/apps/sim/app/account/settings/chat-keys/page.tsx deleted file mode 100644 index 6c20b821d3b..00000000000 --- a/apps/sim/app/account/settings/chat-keys/page.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Suspense } from 'react' -import type { Metadata } from 'next' -import { redirect } from 'next/navigation' -import { getAccountSettingsHref } from '@/components/settings/navigation' -import { getSession } from '@/lib/auth' -import { isHosted } from '@/lib/core/config/env-flags' -import { ChatKeysView } from '@/app/account/settings/chat-keys/chat-keys-view' - -export const metadata: Metadata = { - title: 'Chat keys - Account settings', -} - -export default async function AccountChatKeysPage() { - const session = await getSession() - if (!session?.user) redirect('/login') - if (!isHosted) redirect(getAccountSettingsHref('general')) - - return ( - - - - ) -} diff --git a/apps/sim/app/selfhost/settings/[section]/page.tsx b/apps/sim/app/selfhost/settings/[section]/page.tsx new file mode 100644 index 00000000000..eacc4d54c24 --- /dev/null +++ b/apps/sim/app/selfhost/settings/[section]/page.tsx @@ -0,0 +1,56 @@ +import { Suspense } from 'react' +import type { Metadata } from 'next' +import { notFound, redirect } from 'next/navigation' +import { + getSelfHostSettingsHref, + getSettingsSectionMeta, + parseSettingsPathSection, + SELFHOST_SETTINGS_ITEMS, +} from '@/components/settings/navigation' +import { SelfHostSettingsRenderer } from '@/components/settings/selfhost-settings-renderer' +import { getSession } from '@/lib/auth' +import { isBillingEnabled, isHosted } from '@/lib/core/config/env-flags' + +interface SelfHostSettingsSectionPageProps { + params: Promise<{ section: string }> +} + +export async function generateMetadata({ + params, +}: SelfHostSettingsSectionPageProps): Promise { + const { section } = await params + const parsed = parseSettingsPathSection({ + path: section, + items: SELFHOST_SETTINGS_ITEMS, + defaultSection: null, + }) + const meta = parsed ? getSettingsSectionMeta('selfhost', parsed) : null + return { title: meta ? `${meta.label} - Self-host settings` : 'Self-host settings' } +} + +export default async function SelfHostSettingsSectionPage({ + params, +}: SelfHostSettingsSectionPageProps) { + const session = await getSession() + if (!session?.user) redirect('/login') + + const { section } = await params + const parsed = parseSettingsPathSection({ + path: section, + items: SELFHOST_SETTINGS_ITEMS, + defaultSection: null, + }) + if (!parsed) notFound() + if (parsed === 'billing' && !isBillingEnabled) redirect(getSelfHostSettingsHref('general')) + if (parsed === 'chat-keys' && !isHosted) redirect(getSelfHostSettingsHref('general')) + + /** + * Sections read URL query params via nuqs (which uses `useSearchParams` + * internally), so the renderer must sit under a Suspense boundary. + */ + return ( + + + + ) +} diff --git a/apps/sim/app/selfhost/settings/layout.tsx b/apps/sim/app/selfhost/settings/layout.tsx new file mode 100644 index 00000000000..ad705241995 --- /dev/null +++ b/apps/sim/app/selfhost/settings/layout.tsx @@ -0,0 +1,10 @@ +import { redirect } from 'next/navigation' +import { StandaloneSettingsShell } from '@/components/settings/standalone-settings-shell' +import { getSession } from '@/lib/auth' + +export default async function SelfHostSettingsLayout({ children }: { children: React.ReactNode }) { + const session = await getSession() + if (!session?.user) redirect('/login') + + return {children} +} diff --git a/apps/sim/app/selfhost/settings/page.tsx b/apps/sim/app/selfhost/settings/page.tsx new file mode 100644 index 00000000000..8224aa43280 --- /dev/null +++ b/apps/sim/app/selfhost/settings/page.tsx @@ -0,0 +1,6 @@ +import { redirect } from 'next/navigation' +import { getSelfHostSettingsHref } from '@/components/settings/navigation' + +export default function SelfHostSettingsPage() { + redirect(getSelfHostSettingsHref('general')) +} diff --git a/apps/sim/app/account/settings/chat-keys/chat-keys-view.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/copilot/copilot.tsx similarity index 91% rename from apps/sim/app/account/settings/chat-keys/chat-keys-view.tsx rename to apps/sim/app/workspace/[workspaceId]/settings/components/copilot/copilot.tsx index 475ea46bced..c746dd62828 100644 --- a/apps/sim/app/account/settings/chat-keys/chat-keys-view.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/copilot/copilot.tsx @@ -12,12 +12,9 @@ import { ChipModalHeader, SecretReveal, } from '@sim/emcn' -import { ArrowLeft } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' import { formatDate } from '@sim/utils/formatting' import { Plus } from 'lucide-react' -import { useRouter } from 'next/navigation' -import { getAccountSettingsHref } from '@/components/settings/navigation' import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state' import type { SettingsAction } from '@/app/workspace/[workspaceId]/settings/components/settings-header/settings-header' import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel' @@ -29,7 +26,7 @@ import { useGenerateCopilotKey, } from '@/hooks/queries/copilot-keys' -const logger = createLogger('ChatKeysSettings') +const logger = createLogger('CopilotSettings') /** Formats a key's last-used timestamp, falling back to "Never" when unset. */ function formatLastUsed(dateString?: string | null): string { @@ -38,11 +35,10 @@ function formatLastUsed(dateString?: string | null): string { } /** - * Standalone page for managing the Chat API keys used with the Chat feature. - * Provides functionality to create, view, and delete Chat API keys. + * Copilot Keys management component for handling API keys used with the Copilot feature. + * Provides functionality to create, view, and delete copilot API keys. */ -export function ChatKeysView() { - const router = useRouter() +export function Copilot() { const { data: keys = [], isLoading } = useCopilotKeys() const generateKey = useGenerateCopilotKey() const deleteKeyMutation = useDeleteCopilotKey() @@ -88,7 +84,7 @@ export function ChatKeysView() { setIsCreateDialogOpen(false) } } catch (error) { - logger.error('Failed to generate Chat API key', { error }) + logger.error('Failed to generate copilot API key', { error }) setCreateError('Failed to create API key. Please check your connection and try again.') } } @@ -102,7 +98,7 @@ export function ChatKeysView() { await deleteKeyMutation.mutateAsync({ keyId: keyToDelete.id }) } catch (error) { - logger.error('Failed to delete Chat API key', { error }) + logger.error('Failed to delete copilot API key', { error }) } } @@ -126,19 +122,12 @@ export function ChatKeysView() { return ( <> router.push(getAccountSettingsHref('general')), - }} search={{ value: searchTerm, onChange: setSearchTerm, placeholder: 'Search API keys...', }} actions={actions} - title='Chat keys' - description='Create and manage the API keys that power Chat.' > {isLoading ? null : showEmptyState ? ( Click "Create API key" above to get started diff --git a/apps/sim/app/workspace/[workspaceId]/settings/navigation.test.ts b/apps/sim/app/workspace/[workspaceId]/settings/navigation.test.ts index 8b1ff448e25..b3ed6ee0d1d 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/navigation.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/settings/navigation.test.ts @@ -49,10 +49,12 @@ describe('unified settings navigation', () => { }) it('derives every unified item from exactly one registry entry', () => { - expect(allNavigationItems).toHaveLength(SETTINGS_SECTION_REGISTRY.length) + expect(allNavigationItems).toHaveLength( + SETTINGS_SECTION_REGISTRY.filter(({ unified }) => unified).length + ) for (const item of allNavigationItems) { expect( - SETTINGS_SECTION_REGISTRY.filter(({ unified }) => unified.id === item.id) + SETTINGS_SECTION_REGISTRY.filter(({ unified }) => unified?.id === item.id) ).toHaveLength(1) } }) diff --git a/apps/sim/components/settings/navigation.test.ts b/apps/sim/components/settings/navigation.test.ts index 7d8085d6ea2..0846cab14da 100644 --- a/apps/sim/components/settings/navigation.test.ts +++ b/apps/sim/components/settings/navigation.test.ts @@ -17,6 +17,7 @@ import { parseSettingsPathSection, resolveOrganizationSectionAccess, resolveWorkspaceNavigation, + SELFHOST_SETTINGS_ITEMS, SETTINGS_SECTION_REGISTRY, WORKSPACE_SETTINGS_ITEMS, WORKSPACE_SETTINGS_PATH_ALIASES, @@ -56,6 +57,7 @@ describe('settings navigation boundaries', () => { 'admin', 'mothership', ]) + expect(SELFHOST_SETTINGS_ITEMS.map(({ id }) => id)).toEqual(['general', 'billing', 'chat-keys']) expect(ORGANIZATION_SETTINGS_ITEMS.map(({ id }) => id)).toEqual([ 'members', 'billing', @@ -83,13 +85,18 @@ describe('settings navigation boundaries', () => { }) it('has one registry source for every unified and plane item', () => { - const unifiedIds = SETTINGS_SECTION_REGISTRY.map(({ unified }) => unified.id) + const unifiedIds = SETTINGS_SECTION_REGISTRY.flatMap(({ unified }) => + unified ? [unified.id] : [] + ) const accountIds = SETTINGS_SECTION_REGISTRY.flatMap(({ planes }) => planes?.account ? [planes.account.id] : [] ) const organizationIds = SETTINGS_SECTION_REGISTRY.flatMap(({ planes }) => planes?.organization ? [planes.organization.id] : [] ) + const selfHostIds = SETTINGS_SECTION_REGISTRY.flatMap(({ planes }) => + planes?.selfhost ? [planes.selfhost.id] : [] + ) const workspaceIds = SETTINGS_SECTION_REGISTRY.flatMap(({ planes }) => planes?.workspace ? [planes.workspace.id] : [] ) @@ -97,6 +104,7 @@ describe('settings navigation boundaries', () => { expect(new Set(unifiedIds).size).toBe(unifiedIds.length) expect(new Set(accountIds).size).toBe(accountIds.length) expect(new Set(organizationIds).size).toBe(organizationIds.length) + expect(new Set(selfHostIds).size).toBe(selfHostIds.length) expect(new Set(workspaceIds).size).toBe(workspaceIds.length) expect([...unifiedIds].sort()).toEqual( buildUnifiedSettingsNavigation() @@ -107,6 +115,7 @@ describe('settings navigation boundaries', () => { expect([...organizationIds].sort()).toEqual( ORGANIZATION_SETTINGS_ITEMS.map(({ id }) => id).sort() ) + expect([...selfHostIds].sort()).toEqual(SELFHOST_SETTINGS_ITEMS.map(({ id }) => id).sort()) expect([...workspaceIds].sort()).toEqual(WORKSPACE_SETTINGS_ITEMS.map(({ id }) => id).sort()) }) @@ -179,9 +188,6 @@ describe('settings navigation boundaries', () => { expect(parseAccountPath('/account/settings/apikeys', null)).toBe('api-keys') expect(parseAccountPath('/account/settings/not-a-section', null)).toBeNull() expect(parseAccountPath('/account/settings', 'general')).toBe('general') - // Chat keys is a real page but deliberately not a nav item — with a null - // default it must resolve to nothing so the sidebar highlights no sibling. - expect(parseAccountPath('/account/settings/chat-keys', null)).toBeNull() }) it('parses canonical, aliased, and invalid organization settings paths', () => { diff --git a/apps/sim/components/settings/navigation.ts b/apps/sim/components/settings/navigation.ts index d76cbfe0409..a567a79e317 100644 --- a/apps/sim/components/settings/navigation.ts +++ b/apps/sim/components/settings/navigation.ts @@ -26,10 +26,16 @@ import { McpIcon } from '@/components/icons' import { getEnv, isTruthy } from '@/lib/core/config/env' import { isHosted } from '@/lib/core/config/env-flags' -export type SettingsPlane = 'account' | 'organization' | 'workspace' +export type SettingsPlane = 'account' | 'organization' | 'selfhost' | 'workspace' export type AccountSettingsSection = 'general' | 'billing' | 'api-keys' | 'admin' | 'mothership' +/** + * Settings a self-hoster needs from the managed service: their profile, what + * they pay for, and the Chat keys their own deployment authenticates with. + */ +export type SelfHostSettingsSection = 'general' | 'billing' | 'chat-keys' + export type OrganizationSettingsSection = | 'members' | 'billing' @@ -57,6 +63,7 @@ export type WorkspaceSettingsSection = export type SettingsSection = | AccountSettingsSection | OrganizationSettingsSection + | SelfHostSettingsSection | WorkspaceSettingsSection export type OrganizationSettingsRouteSection = OrganizationSettingsSection | 'unavailable' @@ -132,6 +139,7 @@ interface UnifiedSettingsProjection interface SettingsPlaneSectionMap { account: AccountSettingsSection organization: OrganizationSettingsSection + selfhost: SelfHostSettingsSection workspace: WorkspaceSettingsSection } @@ -153,7 +161,8 @@ export interface SettingsSectionRegistryEntry { label: string icon: ComponentType<{ className?: string }> docsLink?: string - unified: UnifiedSettingsProjection + /** Omit for sections that exist only on a standalone plane. */ + unified?: UnifiedSettingsProjection planes?: SettingsPlaneProjections } @@ -188,6 +197,13 @@ export function getAccountSettingsHref( return withSettingsSearchParams(`/account/settings/${section}`, searchParams) } +export function getSelfHostSettingsHref( + section: SelfHostSettingsSection, + searchParams?: SettingsHrefSearchParams +): string { + return withSettingsSearchParams(`/selfhost/settings/${section}`, searchParams) +} + export function getOrganizationSettingsHref( organizationId: string, section: OrganizationSettingsRouteSection, @@ -267,6 +283,28 @@ export const ACCOUNT_SETTINGS_GROUPS = [ { key: 'platform', title: 'Platform' }, ] as const +/** Planes with their own standalone shell; the workspace plane renders inside the editor. */ +export type StandaloneSettingsPlane = Exclude + +/** + * Per-plane sidebar chrome. Self-host is reached from outside the app (the CLI + * wizard, the README), so it leads with the brand mark rather than a Back link + * into a workspace the visitor may not even be using. + */ +export const SETTINGS_PLANE_CHROME: Record< + StandaloneSettingsPlane, + { label: string; showWordmark: boolean } +> = { + account: { label: 'Account', showWordmark: false }, + organization: { label: 'Organization', showWordmark: false }, + selfhost: { label: 'Self-host', showWordmark: true }, +} + +export const SELFHOST_SETTINGS_GROUPS = [ + { key: 'account', title: 'Account' }, + { key: 'developer', title: 'Developer' }, +] as const + export const ORGANIZATION_SETTINGS_GROUPS = [ { key: 'organization', title: 'Organization' }, { key: 'security', title: 'Security' }, @@ -291,6 +329,7 @@ export const SETTINGS_SECTION_REGISTRY: readonly SettingsSectionRegistryEntry[] }, planes: { account: { id: 'general', group: 'account', order: 0 }, + selfhost: { id: 'general', group: 'account', order: 0 }, }, }, { @@ -354,6 +393,12 @@ export const SETTINGS_SECTION_REGISTRY: readonly SettingsSectionRegistryEntry[] group: 'account', order: 1, }, + selfhost: { + id: 'billing', + description: 'Manage your personal plan, usage, and invoices.', + group: 'account', + order: 1, + }, organization: { id: 'billing', description: 'Manage the organization plan, usage, and invoices.', @@ -479,6 +524,18 @@ export const SETTINGS_SECTION_REGISTRY: readonly SettingsSectionRegistryEntry[] workspace: { id: 'byok', group: 'workspace', order: 2 }, }, }, + { + label: 'Chat keys', + icon: HexSimple, + planes: { + selfhost: { + id: 'chat-keys', + description: 'Manage the model-provider keys that power Chat.', + group: 'developer', + order: 2, + }, + }, + }, { label: 'Sim mailer', icon: Send, @@ -634,15 +691,18 @@ export const SETTINGS_SECTION_REGISTRY: readonly SettingsSectionRegistryEntry[] ] export function buildUnifiedSettingsNavigation(): UnifiedSettingsNavigationItem[] { - return SETTINGS_SECTION_REGISTRY.map(({ label, icon, docsLink, unified }) => { + return SETTINGS_SECTION_REGISTRY.flatMap(({ label, icon, docsLink, unified }) => { + if (!unified) return [] const { group, ...item } = unified - return { - ...item, - label, - icon, - section: group, - ...(docsLink ? { docsLink } : {}), - } + return [ + { + ...item, + label, + icon, + section: group, + ...(docsLink ? { docsLink } : {}), + }, + ] }) } @@ -654,14 +714,22 @@ function buildPlaneSettingsItems( return projection ? [{ entry, projection }] : [] }) .sort((left, right) => left.projection.order - right.projection.order) - .map(({ entry, projection }) => ({ - id: projection.id, - label: projection.label ?? entry.label, - description: projection.description ?? entry.unified.description, - icon: entry.icon, - group: projection.group, - ...(entry.docsLink ? { docsLink: entry.docsLink } : {}), - })) + .map(({ entry, projection }) => { + // A plane-only section carries no unified projection to inherit from, so + // its own description is the only source — missing one is a registry bug. + const description = projection.description ?? entry.unified?.description + if (!description) { + throw new Error(`Settings section "${projection.id}" is missing a description`) + } + return { + id: projection.id, + label: projection.label ?? entry.label, + description, + icon: entry.icon, + group: projection.group, + ...(entry.docsLink ? { docsLink: entry.docsLink } : {}), + } + }) } export const ACCOUNT_SETTINGS_ITEMS: SettingsNavigationItem[] = @@ -670,6 +738,9 @@ export const ACCOUNT_SETTINGS_ITEMS: SettingsNavigationItem[] = buildPlaneSettingsItems('organization') +export const SELFHOST_SETTINGS_ITEMS: SettingsNavigationItem[] = + buildPlaneSettingsItems('selfhost') + export const WORKSPACE_SETTINGS_ITEMS: SettingsNavigationItem[] = buildPlaneSettingsItems('workspace') @@ -681,7 +752,7 @@ export const WORKSPACE_SETTINGS_ITEMS: SettingsNavigationItem = new Set( SETTINGS_SECTION_REGISTRY.flatMap((entry) => - entry.planes?.organization ? [entry.unified.id] : [] + entry.planes?.organization && entry.unified ? [entry.unified.id] : [] ) ) @@ -834,7 +905,9 @@ export function getSettingsSectionMeta( ? ACCOUNT_SETTINGS_ITEMS : plane === 'organization' ? ORGANIZATION_SETTINGS_ITEMS - : WORKSPACE_SETTINGS_ITEMS + : plane === 'selfhost' + ? SELFHOST_SETTINGS_ITEMS + : WORKSPACE_SETTINGS_ITEMS const item = catalog.find((candidate) => candidate.id === section) return item ? { label: item.label, description: item.description, docsLink: item.docsLink } : null } diff --git a/apps/sim/components/settings/selfhost-settings-renderer.tsx b/apps/sim/components/settings/selfhost-settings-renderer.tsx new file mode 100644 index 00000000000..3258c37ebb7 --- /dev/null +++ b/apps/sim/components/settings/selfhost-settings-renderer.tsx @@ -0,0 +1,35 @@ +'use client' + +import { useEffect } from 'react' +import dynamic from 'next/dynamic' +import { usePostHog } from 'posthog-js/react' +import type { SelfHostSettingsSection } from '@/components/settings/navigation' +import { captureEvent } from '@/lib/posthog/client' +import { General } from '@/app/workspace/[workspaceId]/settings/components/general/general' + +const Billing = dynamic(() => + import('@/app/workspace/[workspaceId]/settings/components/billing/billing').then( + (module) => module.Billing + ) +) +const ChatKeys = dynamic(() => + import('@/app/workspace/[workspaceId]/settings/components/copilot/copilot').then( + (module) => module.Copilot + ) +) + +interface SelfHostSettingsRendererProps { + section: SelfHostSettingsSection +} + +export function SelfHostSettingsRenderer({ section }: SelfHostSettingsRendererProps) { + const posthog = usePostHog() + + useEffect(() => { + captureEvent(posthog, 'settings_tab_viewed', { plane: 'selfhost', section }) + }, [posthog, section]) + + if (section === 'general') return + if (section === 'billing') return + return +} diff --git a/apps/sim/components/settings/settings-sidebar.tsx b/apps/sim/components/settings/settings-sidebar.tsx index 175bf35b9d8..af95658ab5a 100644 --- a/apps/sim/components/settings/settings-sidebar.tsx +++ b/apps/sim/components/settings/settings-sidebar.tsx @@ -4,9 +4,24 @@ import { useEffect, useRef, useState } from 'react' import { ChipConfirmModal, chipVariants, cn, Tooltip } from '@sim/emcn' import { ChevronDown } from '@sim/emcn/icons' import { useRouter } from 'next/navigation' -import type { SettingsNavigationItem, SettingsSection } from '@/components/settings/navigation' +import { + SETTINGS_PLANE_CHROME, + type SettingsNavigationItem, + type SettingsSection, + type StandaloneSettingsPlane, +} from '@/components/settings/navigation' +import { SimWordmark } from '@/app/(landing)/components/navbar/components' import { useSettingsDirtyStore } from '@/stores/settings/dirty/store' +/** + * The marketing landing page. `?home` is required: the proxy bounces a + * signed-in user off `/` to `/workspace` unless the param is present. + */ +const LANDING_HREF = '/?home' + +/** Where the Back chip goes on planes that don't show the wordmark. */ +const WORKSPACE_HREF = '/workspace' + interface SettingsNavigationGroup { key: string title: string @@ -18,9 +33,8 @@ interface SidebarSettingsItem
} interface SettingsSidebarProps
{ - /** `null` when the route is a nested page that is not itself a nav item — nothing highlights. */ - activeSection: string | null - backHref: string + activeSection: string + plane: StandaloneSettingsPlane groups: readonly SettingsNavigationGroup[] hrefForSection: (section: Section) => string items: readonly SidebarSettingsItem
[] @@ -48,7 +62,7 @@ function SidebarTooltip({ export function SettingsSidebar
({ activeSection, - backHref, + plane, groups, hrefForSection, items, @@ -83,18 +97,30 @@ export function SettingsSidebar
({ return ( <>
- + {/* Both stay buttons, not Links: leaving settings must run the unsaved-changes guard. */} + {SETTINGS_PLANE_CHROME[plane].showWordmark ? ( - + ) : ( + + + + )}
{ + if (item.id === 'billing' && !isBillingEnabled) return false + // Chat keys are issued by the managed service, so there are none to list on + // a self-hosted deployment — useCopilotKeys is `enabled: isHosted` for the + // same reason. Self-hosters manage their keys on sim.ai. + if (item.id === 'chat-keys' && !isHosted) return false + return true + }) + const selfHostSection = parseSettingsPathSection({ + path: pathname, + items: SELFHOST_SETTINGS_ITEMS, + defaultSection: 'general', + }) const accountSection = parseSettingsPathSection({ path: pathname, items: ACCOUNT_SETTINGS_ITEMS, @@ -75,25 +99,25 @@ export function StandaloneSettingsShell(props: StandaloneSettingsShellProps) { defaultSection: 'members', aliases: ORGANIZATION_SETTINGS_PATH_ALIASES, }) - const activeSection = plane === 'account' ? accountSection : organizationSection - /** - * The sidebar highlights only an exact nav match. A nested route that is not - * itself a nav item (e.g. /account/settings/chat-keys) would otherwise fall back - * to `defaultSection` and light up an unrelated sibling — reading as though the - * page lived inside it. The section above keeps its default for the title - * provider, which every page can still override. - */ - const accountSidebarSection = parseSettingsPathSection({ - path: pathname, - items: ACCOUNT_SETTINGS_ITEMS, - defaultSection: null, - aliases: ACCOUNT_SETTINGS_PATH_ALIASES, - }) + const activeSection = + plane === 'account' + ? accountSection + : plane === 'selfhost' + ? selfHostSection + : organizationSection const sidebar = - plane === 'account' ? ( + plane === 'selfhost' ? ( + + ) : plane === 'account' ? ( getOrganizationSettingsHref(props.organizationId, section)} items={organizationItems} @@ -113,7 +137,7 @@ export function StandaloneSettingsShell(props: StandaloneSettingsShellProps) {
diff --git a/apps/sim/lib/posthog/events.ts b/apps/sim/lib/posthog/events.ts index 723717d1d36..ce7c2657dbd 100644 --- a/apps/sim/lib/posthog/events.ts +++ b/apps/sim/lib/posthog/events.ts @@ -390,7 +390,7 @@ export interface PostHogEventMap { } settings_tab_viewed: { - plane: 'account' | 'organization' | 'workspace' + plane: 'account' | 'organization' | 'selfhost' | 'workspace' section: string } diff --git a/package.json b/package.json index 08457264a00..50e16354786 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "mship:check": "bun run scripts/generate-mship-contracts.ts --check", "skills:sync": "bun run scripts/sync-skills.ts", "skills:check": "bun run scripts/sync-skills.ts --check", - "setup": "bun run scripts/setup/index.ts setup", + "setup": "bun install && bun run scripts/setup/index.ts setup", "sim": "bun run scripts/setup/index.ts", "doctor": "bun run scripts/setup/index.ts doctor", "agent-stream-docs:generate": "bun run scripts/sync-agent-stream-docs.ts",