Skip to content

Commit 75b8b6f

Browse files
feat(settings): self-host settings plane, Sim wordmark in sidebar (#5990)
* feat(settings): self-host settings plane, Sim wordmark in sidebar Chat keys were only reachable at a standalone /account/settings/chat-keys page that nothing linked to. They now live on a dedicated self-host plane alongside the two other settings a self-hoster needs from the managed service. - new /selfhost/settings/{general,billing,chat-keys} plane, open to any signed-in user - chat keys move off the account plane entirely; no isHosted gate - registry `unified` projection is now optional (mirroring `planes`), so a section can opt out of the editor sidebar - plane items resolve their own description and throw when one is missing, since a plane-only section has no unified projection to inherit from - settings sidebar shows the Sim wordmark linking to /?home instead of a Back chip; drops the now-dead backHref prop - `bun run setup` runs `bun install` first so a fresh clone is one command * docs(readme): point Chat keys at the self-host settings plane The account-plane URL stopped resolving when chat keys moved to /selfhost/settings. * improvement(settings): make the sidebar wordmark a plane attribute Replacing the Back chip everywhere was too broad — account and organization are reached from inside the app, so Back is right there. Self-host is reached from outside it (the CLI wizard, the README), so it leads with the brand mark instead. SETTINGS_PLANE_CHROME declares that per plane, keyed on StandaloneSettingsPlane so adding a plane forces the decision rather than defaulting silently. It also absorbs the shell's parallel plane-label map. * fix(settings): hide self-host Chat keys on non-hosted deployments Chat keys are issued by the managed service and useCopilotKeys is `enabled: isHosted` for that reason. Moving the section onto the self-host plane dropped its hosted gate, so a self-hosted deployment rendered a Chat keys nav item whose list could never populate. Restores the gate on the plane that now owns the URL. sim.ai is unaffected — the section stays visible there to every signed-in user, which is the surface self-hosters are pointed at.
1 parent 23bc210 commit 75b8b6f

14 files changed

Lines changed: 309 additions & 112 deletions

File tree

README.md

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,12 @@
2727
### Self-hosted
2828

2929
```bash
30-
npx simstudio
30+
git clone https://github.com/simstudioai/sim.git && cd sim
31+
bun run setup
3132
```
3233

3334
Open [http://localhost:3000](http://localhost:3000)
3435

35-
Docker must be installed and running. Use `-p, --port <port>` to run Sim on a different port, or `--no-pull` to skip pulling the latest Docker images.
36-
3736
<p align="center">
3837
<img src="apps/sim/public/static/readme-platform.png" alt="The Sim platform — chat on the left, the visual workflow builder on the right" width="100%"/>
3938
</p>
@@ -77,12 +76,6 @@ Docker must be installed and running. Use `-p, --port <port>` to run Sim on a di
7776

7877
**Requirements:** [Bun](https://bun.sh/) and [Docker](https://www.docker.com/).
7978

80-
```bash
81-
git clone https://github.com/simstudioai/sim.git && cd sim
82-
bun install
83-
bun run setup
84-
```
85-
8679
`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:
8780

8881
- **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:
110103

111104
## Chat API Keys
112105

113-
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).
106+
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).
114107

115108
## Environment Variables
116109

apps/sim/app/account/settings/chat-keys/page.tsx

Lines changed: 0 additions & 23 deletions
This file was deleted.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { Suspense } from 'react'
2+
import type { Metadata } from 'next'
3+
import { notFound, redirect } from 'next/navigation'
4+
import {
5+
getSelfHostSettingsHref,
6+
getSettingsSectionMeta,
7+
parseSettingsPathSection,
8+
SELFHOST_SETTINGS_ITEMS,
9+
} from '@/components/settings/navigation'
10+
import { SelfHostSettingsRenderer } from '@/components/settings/selfhost-settings-renderer'
11+
import { getSession } from '@/lib/auth'
12+
import { isBillingEnabled, isHosted } from '@/lib/core/config/env-flags'
13+
14+
interface SelfHostSettingsSectionPageProps {
15+
params: Promise<{ section: string }>
16+
}
17+
18+
export async function generateMetadata({
19+
params,
20+
}: SelfHostSettingsSectionPageProps): Promise<Metadata> {
21+
const { section } = await params
22+
const parsed = parseSettingsPathSection({
23+
path: section,
24+
items: SELFHOST_SETTINGS_ITEMS,
25+
defaultSection: null,
26+
})
27+
const meta = parsed ? getSettingsSectionMeta('selfhost', parsed) : null
28+
return { title: meta ? `${meta.label} - Self-host settings` : 'Self-host settings' }
29+
}
30+
31+
export default async function SelfHostSettingsSectionPage({
32+
params,
33+
}: SelfHostSettingsSectionPageProps) {
34+
const session = await getSession()
35+
if (!session?.user) redirect('/login')
36+
37+
const { section } = await params
38+
const parsed = parseSettingsPathSection({
39+
path: section,
40+
items: SELFHOST_SETTINGS_ITEMS,
41+
defaultSection: null,
42+
})
43+
if (!parsed) notFound()
44+
if (parsed === 'billing' && !isBillingEnabled) redirect(getSelfHostSettingsHref('general'))
45+
if (parsed === 'chat-keys' && !isHosted) redirect(getSelfHostSettingsHref('general'))
46+
47+
/**
48+
* Sections read URL query params via nuqs (which uses `useSearchParams`
49+
* internally), so the renderer must sit under a Suspense boundary.
50+
*/
51+
return (
52+
<Suspense fallback={null}>
53+
<SelfHostSettingsRenderer section={parsed} />
54+
</Suspense>
55+
)
56+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { redirect } from 'next/navigation'
2+
import { StandaloneSettingsShell } from '@/components/settings/standalone-settings-shell'
3+
import { getSession } from '@/lib/auth'
4+
5+
export default async function SelfHostSettingsLayout({ children }: { children: React.ReactNode }) {
6+
const session = await getSession()
7+
if (!session?.user) redirect('/login')
8+
9+
return <StandaloneSettingsShell plane='selfhost'>{children}</StandaloneSettingsShell>
10+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { redirect } from 'next/navigation'
2+
import { getSelfHostSettingsHref } from '@/components/settings/navigation'
3+
4+
export default function SelfHostSettingsPage() {
5+
redirect(getSelfHostSettingsHref('general'))
6+
}

apps/sim/app/account/settings/chat-keys/chat-keys-view.tsx renamed to apps/sim/app/workspace/[workspaceId]/settings/components/copilot/copilot.tsx

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,9 @@ import {
1212
ChipModalHeader,
1313
SecretReveal,
1414
} from '@sim/emcn'
15-
import { ArrowLeft } from '@sim/emcn/icons'
1615
import { createLogger } from '@sim/logger'
1716
import { formatDate } from '@sim/utils/formatting'
1817
import { Plus } from 'lucide-react'
19-
import { useRouter } from 'next/navigation'
20-
import { getAccountSettingsHref } from '@/components/settings/navigation'
2118
import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
2219
import type { SettingsAction } from '@/app/workspace/[workspaceId]/settings/components/settings-header/settings-header'
2320
import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel'
@@ -29,7 +26,7 @@ import {
2926
useGenerateCopilotKey,
3027
} from '@/hooks/queries/copilot-keys'
3128

32-
const logger = createLogger('ChatKeysSettings')
29+
const logger = createLogger('CopilotSettings')
3330

3431
/** Formats a key's last-used timestamp, falling back to "Never" when unset. */
3532
function formatLastUsed(dateString?: string | null): string {
@@ -38,11 +35,10 @@ function formatLastUsed(dateString?: string | null): string {
3835
}
3936

4037
/**
41-
* Standalone page for managing the Chat API keys used with the Chat feature.
42-
* Provides functionality to create, view, and delete Chat API keys.
38+
* Copilot Keys management component for handling API keys used with the Copilot feature.
39+
* Provides functionality to create, view, and delete copilot API keys.
4340
*/
44-
export function ChatKeysView() {
45-
const router = useRouter()
41+
export function Copilot() {
4642
const { data: keys = [], isLoading } = useCopilotKeys()
4743
const generateKey = useGenerateCopilotKey()
4844
const deleteKeyMutation = useDeleteCopilotKey()
@@ -88,7 +84,7 @@ export function ChatKeysView() {
8884
setIsCreateDialogOpen(false)
8985
}
9086
} catch (error) {
91-
logger.error('Failed to generate Chat API key', { error })
87+
logger.error('Failed to generate copilot API key', { error })
9288
setCreateError('Failed to create API key. Please check your connection and try again.')
9389
}
9490
}
@@ -102,7 +98,7 @@ export function ChatKeysView() {
10298

10399
await deleteKeyMutation.mutateAsync({ keyId: keyToDelete.id })
104100
} catch (error) {
105-
logger.error('Failed to delete Chat API key', { error })
101+
logger.error('Failed to delete copilot API key', { error })
106102
}
107103
}
108104

@@ -126,19 +122,12 @@ export function ChatKeysView() {
126122
return (
127123
<>
128124
<SettingsPanel
129-
back={{
130-
text: 'Account',
131-
icon: ArrowLeft,
132-
onSelect: () => router.push(getAccountSettingsHref('general')),
133-
}}
134125
search={{
135126
value: searchTerm,
136127
onChange: setSearchTerm,
137128
placeholder: 'Search API keys...',
138129
}}
139130
actions={actions}
140-
title='Chat keys'
141-
description='Create and manage the API keys that power Chat.'
142131
>
143132
{isLoading ? null : showEmptyState ? (
144133
<SettingsEmptyState>Click "Create API key" above to get started</SettingsEmptyState>

apps/sim/app/workspace/[workspaceId]/settings/navigation.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,12 @@ describe('unified settings navigation', () => {
4949
})
5050

5151
it('derives every unified item from exactly one registry entry', () => {
52-
expect(allNavigationItems).toHaveLength(SETTINGS_SECTION_REGISTRY.length)
52+
expect(allNavigationItems).toHaveLength(
53+
SETTINGS_SECTION_REGISTRY.filter(({ unified }) => unified).length
54+
)
5355
for (const item of allNavigationItems) {
5456
expect(
55-
SETTINGS_SECTION_REGISTRY.filter(({ unified }) => unified.id === item.id)
57+
SETTINGS_SECTION_REGISTRY.filter(({ unified }) => unified?.id === item.id)
5658
).toHaveLength(1)
5759
}
5860
})

apps/sim/components/settings/navigation.test.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
parseSettingsPathSection,
1818
resolveOrganizationSectionAccess,
1919
resolveWorkspaceNavigation,
20+
SELFHOST_SETTINGS_ITEMS,
2021
SETTINGS_SECTION_REGISTRY,
2122
WORKSPACE_SETTINGS_ITEMS,
2223
WORKSPACE_SETTINGS_PATH_ALIASES,
@@ -56,6 +57,7 @@ describe('settings navigation boundaries', () => {
5657
'admin',
5758
'mothership',
5859
])
60+
expect(SELFHOST_SETTINGS_ITEMS.map(({ id }) => id)).toEqual(['general', 'billing', 'chat-keys'])
5961
expect(ORGANIZATION_SETTINGS_ITEMS.map(({ id }) => id)).toEqual([
6062
'members',
6163
'billing',
@@ -83,20 +85,26 @@ describe('settings navigation boundaries', () => {
8385
})
8486

8587
it('has one registry source for every unified and plane item', () => {
86-
const unifiedIds = SETTINGS_SECTION_REGISTRY.map(({ unified }) => unified.id)
88+
const unifiedIds = SETTINGS_SECTION_REGISTRY.flatMap(({ unified }) =>
89+
unified ? [unified.id] : []
90+
)
8791
const accountIds = SETTINGS_SECTION_REGISTRY.flatMap(({ planes }) =>
8892
planes?.account ? [planes.account.id] : []
8993
)
9094
const organizationIds = SETTINGS_SECTION_REGISTRY.flatMap(({ planes }) =>
9195
planes?.organization ? [planes.organization.id] : []
9296
)
97+
const selfHostIds = SETTINGS_SECTION_REGISTRY.flatMap(({ planes }) =>
98+
planes?.selfhost ? [planes.selfhost.id] : []
99+
)
93100
const workspaceIds = SETTINGS_SECTION_REGISTRY.flatMap(({ planes }) =>
94101
planes?.workspace ? [planes.workspace.id] : []
95102
)
96103

97104
expect(new Set(unifiedIds).size).toBe(unifiedIds.length)
98105
expect(new Set(accountIds).size).toBe(accountIds.length)
99106
expect(new Set(organizationIds).size).toBe(organizationIds.length)
107+
expect(new Set(selfHostIds).size).toBe(selfHostIds.length)
100108
expect(new Set(workspaceIds).size).toBe(workspaceIds.length)
101109
expect([...unifiedIds].sort()).toEqual(
102110
buildUnifiedSettingsNavigation()
@@ -107,6 +115,7 @@ describe('settings navigation boundaries', () => {
107115
expect([...organizationIds].sort()).toEqual(
108116
ORGANIZATION_SETTINGS_ITEMS.map(({ id }) => id).sort()
109117
)
118+
expect([...selfHostIds].sort()).toEqual(SELFHOST_SETTINGS_ITEMS.map(({ id }) => id).sort())
110119
expect([...workspaceIds].sort()).toEqual(WORKSPACE_SETTINGS_ITEMS.map(({ id }) => id).sort())
111120
})
112121

@@ -179,9 +188,6 @@ describe('settings navigation boundaries', () => {
179188
expect(parseAccountPath('/account/settings/apikeys', null)).toBe('api-keys')
180189
expect(parseAccountPath('/account/settings/not-a-section', null)).toBeNull()
181190
expect(parseAccountPath('/account/settings', 'general')).toBe('general')
182-
// Chat keys is a real page but deliberately not a nav item — with a null
183-
// default it must resolve to nothing so the sidebar highlights no sibling.
184-
expect(parseAccountPath('/account/settings/chat-keys', null)).toBeNull()
185191
})
186192

187193
it('parses canonical, aliased, and invalid organization settings paths', () => {

0 commit comments

Comments
 (0)