Adam 1 месяц назад
Родитель
Сommit
8bcbfd6396

+ 24 - 19
packages/app/src/app.tsx

@@ -14,6 +14,7 @@ import { PermissionProvider } from "@/context/permission"
 import { LayoutProvider } from "@/context/layout"
 import { GlobalSDKProvider } from "@/context/global-sdk"
 import { ServerProvider, useServer } from "@/context/server"
+import { SettingsProvider } from "@/context/settings"
 import { TerminalProvider } from "@/context/terminal"
 import { PromptProvider } from "@/context/prompt"
 import { FileProvider } from "@/context/file"
@@ -82,15 +83,17 @@ export function AppInterface(props: { defaultUrl?: string }) {
           <GlobalSyncProvider>
             <Router
               root={(props) => (
-                <PermissionProvider>
-                  <LayoutProvider>
-                    <NotificationProvider>
-                      <CommandProvider>
-                        <Layout>{props.children}</Layout>
-                      </CommandProvider>
-                    </NotificationProvider>
-                  </LayoutProvider>
-                </PermissionProvider>
+                <SettingsProvider>
+                  <PermissionProvider>
+                    <LayoutProvider>
+                      <NotificationProvider>
+                        <CommandProvider>
+                          <Layout>{props.children}</Layout>
+                        </CommandProvider>
+                      </NotificationProvider>
+                    </LayoutProvider>
+                  </PermissionProvider>
+                </SettingsProvider>
               )}
             >
               <Route
@@ -105,16 +108,18 @@ export function AppInterface(props: { defaultUrl?: string }) {
                 <Route path="/" component={() => <Navigate href="session" />} />
                 <Route
                   path="/session/:id?"
-                  component={() => (
-                    <TerminalProvider>
-                      <FileProvider>
-                        <PromptProvider>
-                          <Suspense fallback={<Loading />}>
-                            <Session />
-                          </Suspense>
-                        </PromptProvider>
-                      </FileProvider>
-                    </TerminalProvider>
+                  component={(p) => (
+                    <Show when={p.params.id ?? "new"} keyed>
+                      <TerminalProvider>
+                        <FileProvider>
+                          <PromptProvider>
+                            <Suspense fallback={<Loading />}>
+                              <Session />
+                            </Suspense>
+                          </PromptProvider>
+                        </FileProvider>
+                      </TerminalProvider>
+                    </Show>
                   )}
                 />
               </Route>

+ 87 - 0
packages/app/src/components/dialog-settings.tsx

@@ -0,0 +1,87 @@
+import { Component, createSignal } from "solid-js"
+import { Dialog } from "@opencode-ai/ui/dialog"
+import { Tabs } from "@opencode-ai/ui/tabs"
+import { Icon } from "@opencode-ai/ui/icon"
+import { TextField } from "@opencode-ai/ui/text-field"
+import { SettingsGeneral } from "./settings-general"
+import { SettingsKeybinds } from "./settings-keybinds"
+import { SettingsPermissions } from "./settings-permissions"
+import { SettingsProviders } from "./settings-providers"
+import { SettingsModels } from "./settings-models"
+import { SettingsAgents } from "./settings-agents"
+import { SettingsCommands } from "./settings-commands"
+import { SettingsMcp } from "./settings-mcp"
+
+export const DialogSettings: Component = () => {
+  const [search, setSearch] = createSignal("")
+
+  return (
+    <Dialog size="large">
+      <Tabs orientation="vertical" variant="settings" defaultValue="general" class="h-full settings-dialog">
+        <Tabs.List>
+          <div class="settings-dialog__search px-3 pb-3">
+            <TextField placeholder="Search" value={search()} onChange={setSearch} variant="normal" />
+          </div>
+          <Tabs.SectionTitle>Desktop</Tabs.SectionTitle>
+          <Tabs.Trigger value="general">
+            <Icon name="settings-gear" />
+            General
+          </Tabs.Trigger>
+          <Tabs.Trigger value="shortcuts">
+            <Icon name="console" />
+            Shortcuts
+          </Tabs.Trigger>
+          <Tabs.SectionTitle>Server</Tabs.SectionTitle>
+          <Tabs.Trigger value="permissions">
+            <Icon name="checklist" />
+            Permissions
+          </Tabs.Trigger>
+          <Tabs.Trigger value="providers">
+            <Icon name="server" />
+            Providers
+          </Tabs.Trigger>
+          <Tabs.Trigger value="models">
+            <Icon name="brain" />
+            Models
+          </Tabs.Trigger>
+          <Tabs.Trigger value="agents">
+            <Icon name="task" />
+            Agents
+          </Tabs.Trigger>
+          <Tabs.Trigger value="commands">
+            <Icon name="console" />
+            Commands
+          </Tabs.Trigger>
+          <Tabs.Trigger value="mcp">
+            <Icon name="mcp" />
+            MCP
+          </Tabs.Trigger>
+        </Tabs.List>
+        <Tabs.Content value="general" class="no-scrollbar">
+          <SettingsGeneral />
+        </Tabs.Content>
+        <Tabs.Content value="shortcuts" class="no-scrollbar">
+          <SettingsKeybinds />
+        </Tabs.Content>
+        <Tabs.Content value="permissions" class="no-scrollbar">
+          <SettingsPermissions />
+        </Tabs.Content>
+        <Tabs.Content value="providers" class="no-scrollbar">
+          <SettingsProviders />
+        </Tabs.Content>
+        <Tabs.Content value="models" class="no-scrollbar">
+          <SettingsModels />
+        </Tabs.Content>
+        <Tabs.Content value="agents" class="no-scrollbar">
+          <SettingsAgents />
+        </Tabs.Content>
+        <Tabs.Content value="commands" class="no-scrollbar">
+          <SettingsCommands />
+        </Tabs.Content>
+        <Tabs.Content value="mcp" class="no-scrollbar">
+          <SettingsMcp />
+        </Tabs.Content>
+      </Tabs>
+    </Dialog>
+  )
+}

+ 12 - 0
packages/app/src/components/settings-agents.tsx

@@ -0,0 +1,12 @@
+import { Component } from "solid-js"
+
+export const SettingsAgents: Component = () => {
+  return (
+    <div class="flex flex-col h-full overflow-y-auto">
+      <div class="flex flex-col gap-6 p-6 max-w-[600px]">
+        <h2 class="text-16-medium text-text-strong">Agents</h2>
+        <p class="text-14-regular text-text-weak">Agent settings will be configurable here.</p>
+      </div>
+    </div>
+  )
+}

+ 12 - 0
packages/app/src/components/settings-commands.tsx

@@ -0,0 +1,12 @@
+import { Component } from "solid-js"
+
+export const SettingsCommands: Component = () => {
+  return (
+    <div class="flex flex-col h-full overflow-y-auto">
+      <div class="flex flex-col gap-6 p-6 max-w-[600px]">
+        <h2 class="text-16-medium text-text-strong">Commands</h2>
+        <p class="text-14-regular text-text-weak">Command settings will be configurable here.</p>
+      </div>
+    </div>
+  )
+}

+ 134 - 0
packages/app/src/components/settings-general.tsx

@@ -0,0 +1,134 @@
+import { Component, createMemo, type JSX } from "solid-js"
+import { Select } from "@opencode-ai/ui/select"
+import { Switch } from "@opencode-ai/ui/switch"
+import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
+import { useSettings } from "@/context/settings"
+
+export const SettingsGeneral: Component = () => {
+  const theme = useTheme()
+  const settings = useSettings()
+
+  const themeOptions = createMemo(() =>
+    Object.entries(theme.themes()).map(([id, def]) => ({ id, name: def.name ?? id })),
+  )
+
+  const colorSchemeOptions: { value: ColorScheme; label: string }[] = [
+    { value: "system", label: "System setting" },
+    { value: "light", label: "Light" },
+    { value: "dark", label: "Dark" },
+  ]
+
+  const fontOptions = [
+    { value: "ibm-plex-mono", label: "IBM Plex Mono" },
+    { value: "fira-code", label: "Fira Code" },
+    { value: "jetbrains-mono", label: "JetBrains Mono" },
+    { value: "source-code-pro", label: "Source Code Pro" },
+  ]
+
+  return (
+    <div class="flex flex-col h-full overflow-y-auto no-scrollbar">
+      <div class="flex flex-col gap-8 p-8 max-w-[720px]">
+        {/* Header */}
+        <h2 class="text-16-medium text-text-strong">General</h2>
+
+        {/* Appearance Section */}
+        <div class="flex flex-col gap-1">
+          <h3 class="text-14-medium text-text-strong pb-2">Appearance</h3>
+
+          <SettingsRow title="Appearance" description="Customise how OpenCode looks on your device">
+            <Select
+              options={colorSchemeOptions}
+              current={colorSchemeOptions.find((o) => o.value === theme.colorScheme())}
+              value={(o) => o.value}
+              label={(o) => o.label}
+              onSelect={(option) => option && theme.setColorScheme(option.value)}
+              variant="secondary"
+              size="small"
+            />
+          </SettingsRow>
+
+          <SettingsRow
+            title="Theme"
+            description={
+              <>
+                Customise how OpenCode is themed.{" "}
+                <a href="#" class="text-text-interactive-base">
+                  Learn more
+                </a>
+              </>
+            }
+          >
+            <Select
+              options={themeOptions()}
+              current={themeOptions().find((o) => o.id === theme.themeId())}
+              value={(o) => o.id}
+              label={(o) => o.name}
+              onSelect={(option) => option && theme.setTheme(option.id)}
+              variant="secondary"
+              size="small"
+            />
+          </SettingsRow>
+
+          <SettingsRow title="Font" description="Customise the mono font used in code blocks">
+            <Select
+              options={fontOptions}
+              current={fontOptions.find((o) => o.value === settings.appearance.font())}
+              value={(o) => o.value}
+              label={(o) => o.label}
+              onSelect={(option) => option && settings.appearance.setFont(option.value)}
+              variant="secondary"
+              size="small"
+            />
+          </SettingsRow>
+        </div>
+
+        {/* System notifications Section */}
+        <div class="flex flex-col gap-1">
+          <h3 class="text-14-medium text-text-strong pb-2">System notifications</h3>
+
+          <SettingsRow
+            title="Agent"
+            description="Show system notification when the agent is complete or needs attention"
+          >
+            <Switch
+              checked={settings.notifications.agent()}
+              onChange={(checked) => settings.notifications.setAgent(checked)}
+            />
+          </SettingsRow>
+
+          <SettingsRow title="Permissions" description="Show system notification when a permission is required">
+            <Switch
+              checked={settings.notifications.permissions()}
+              onChange={(checked) => settings.notifications.setPermissions(checked)}
+            />
+          </SettingsRow>
+
+          <SettingsRow title="Errors" description="Show system notification when an error occurs">
+            <Switch
+              checked={settings.notifications.errors()}
+              onChange={(checked) => settings.notifications.setErrors(checked)}
+            />
+          </SettingsRow>
+        </div>
+      </div>
+    </div>
+  )
+}
+
+interface SettingsRowProps {
+  title: string
+  description: string | JSX.Element
+  children: JSX.Element
+}
+
+const SettingsRow: Component<SettingsRowProps> = (props) => {
+  return (
+    <div class="flex items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
+      <div class="flex flex-col gap-0.5">
+        <span class="text-14-medium text-text-strong">{props.title}</span>
+        <span class="text-12-regular text-text-weak">{props.description}</span>
+      </div>
+      <div class="flex-shrink-0">{props.children}</div>
+    </div>
+  )
+}

+ 12 - 0
packages/app/src/components/settings-keybinds.tsx

@@ -0,0 +1,12 @@
+import { Component } from "solid-js"
+
+export const SettingsKeybinds: Component = () => {
+  return (
+    <div class="flex flex-col h-full overflow-y-auto">
+      <div class="flex flex-col gap-6 p-6 max-w-[600px]">
+        <h2 class="text-16-medium text-text-strong">Shortcuts</h2>
+        <p class="text-14-regular text-text-weak">Keyboard shortcuts will be configurable here.</p>
+      </div>
+    </div>
+  )
+}

+ 12 - 0
packages/app/src/components/settings-mcp.tsx

@@ -0,0 +1,12 @@
+import { Component } from "solid-js"
+
+export const SettingsMcp: Component = () => {
+  return (
+    <div class="flex flex-col h-full overflow-y-auto">
+      <div class="flex flex-col gap-6 p-6 max-w-[600px]">
+        <h2 class="text-16-medium text-text-strong">MCP</h2>
+        <p class="text-14-regular text-text-weak">MCP settings will be configurable here.</p>
+      </div>
+    </div>
+  )
+}

+ 12 - 0
packages/app/src/components/settings-models.tsx

@@ -0,0 +1,12 @@
+import { Component } from "solid-js"
+
+export const SettingsModels: Component = () => {
+  return (
+    <div class="flex flex-col h-full overflow-y-auto">
+      <div class="flex flex-col gap-6 p-6 max-w-[600px]">
+        <h2 class="text-16-medium text-text-strong">Models</h2>
+        <p class="text-14-regular text-text-weak">Model settings will be configurable here.</p>
+      </div>
+    </div>
+  )
+}

+ 12 - 0
packages/app/src/components/settings-permissions.tsx

@@ -0,0 +1,12 @@
+import { Component } from "solid-js"
+
+export const SettingsPermissions: Component = () => {
+  return (
+    <div class="flex flex-col h-full overflow-y-auto">
+      <div class="flex flex-col gap-6 p-6 max-w-[600px]">
+        <h2 class="text-16-medium text-text-strong">Permissions</h2>
+        <p class="text-14-regular text-text-weak">Permission settings will be configurable here.</p>
+      </div>
+    </div>
+  )
+}

+ 12 - 0
packages/app/src/components/settings-providers.tsx

@@ -0,0 +1,12 @@
+import { Component } from "solid-js"
+
+export const SettingsProviders: Component = () => {
+  return (
+    <div class="flex flex-col h-full overflow-y-auto">
+      <div class="flex flex-col gap-6 p-6 max-w-[600px]">
+        <h2 class="text-16-medium text-text-strong">Providers</h2>
+        <p class="text-14-regular text-text-weak">Provider settings will be configurable here.</p>
+      </div>
+    </div>
+  )
+}

+ 103 - 0
packages/app/src/context/settings.tsx

@@ -0,0 +1,103 @@
+import { createStore } from "solid-js/store"
+import { createMemo } from "solid-js"
+import { createSimpleContext } from "@opencode-ai/ui/context"
+import { persisted } from "@/utils/persist"
+
+export interface NotificationSettings {
+  agent: boolean
+  permissions: boolean
+  errors: boolean
+}
+
+export interface Settings {
+  general: {
+    autoSave: boolean
+  }
+  appearance: {
+    fontSize: number
+    font: string
+  }
+  keybinds: Record<string, string>
+  permissions: {
+    autoApprove: boolean
+  }
+  notifications: NotificationSettings
+}
+
+const defaultSettings: Settings = {
+  general: {
+    autoSave: true,
+  },
+  appearance: {
+    fontSize: 14,
+    font: "ibm-plex-mono",
+  },
+  keybinds: {},
+  permissions: {
+    autoApprove: false,
+  },
+  notifications: {
+    agent: false,
+    permissions: false,
+    errors: false,
+  },
+}
+
+export const { use: useSettings, provider: SettingsProvider } = createSimpleContext({
+  name: "Settings",
+  init: () => {
+    const [store, setStore, _, ready] = persisted("settings.v1", createStore<Settings>(defaultSettings))
+
+    return {
+      ready,
+      get current() {
+        return store
+      },
+      general: {
+        autoSave: createMemo(() => store.general?.autoSave ?? defaultSettings.general.autoSave),
+        setAutoSave(value: boolean) {
+          setStore("general", "autoSave", value)
+        },
+      },
+      appearance: {
+        fontSize: createMemo(() => store.appearance?.fontSize ?? defaultSettings.appearance.fontSize),
+        setFontSize(value: number) {
+          setStore("appearance", "fontSize", value)
+        },
+        font: createMemo(() => store.appearance?.font ?? defaultSettings.appearance.font),
+        setFont(value: string) {
+          setStore("appearance", "font", value)
+        },
+      },
+      keybinds: {
+        get: (action: string) => store.keybinds?.[action],
+        set(action: string, keybind: string) {
+          setStore("keybinds", action, keybind)
+        },
+        reset(action: string) {
+          setStore("keybinds", action, undefined!)
+        },
+      },
+      permissions: {
+        autoApprove: createMemo(() => store.permissions?.autoApprove ?? defaultSettings.permissions.autoApprove),
+        setAutoApprove(value: boolean) {
+          setStore("permissions", "autoApprove", value)
+        },
+      },
+      notifications: {
+        agent: createMemo(() => store.notifications?.agent ?? defaultSettings.notifications.agent),
+        setAgent(value: boolean) {
+          setStore("notifications", "agent", value)
+        },
+        permissions: createMemo(() => store.notifications?.permissions ?? defaultSettings.notifications.permissions),
+        setPermissions(value: boolean) {
+          setStore("notifications", "permissions", value)
+        },
+        errors: createMemo(() => store.notifications?.errors ?? defaultSettings.notifications.errors),
+        setErrors(value: boolean) {
+          setStore("notifications", "errors", value)
+        },
+      },
+    }
+  },
+})

+ 5 - 0
packages/app/src/pages/layout.tsx

@@ -59,6 +59,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
 import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
 import { DialogSelectProvider } from "@/components/dialog-select-provider"
 import { DialogSelectServer } from "@/components/dialog-select-server"
+import { DialogSettings } from "@/components/dialog-settings"
 import { useCommand, type CommandOption } from "@/context/command"
 import { ConstrainDragXAxis } from "@/utils/solid-dnd"
 import { navStart } from "@/utils/perf"
@@ -880,6 +881,10 @@ export default function Layout(props: ParentProps) {
     dialog.show(() => <DialogSelectServer />)
   }
 
+  function openSettings() {
+    dialog.show(() => <DialogSettings />)
+  }
+
   function navigateToProject(directory: string | undefined) {
     if (!directory) return
     server.projects.touch(directory)

+ 15 - 1
packages/ui/src/components/dialog.css

@@ -30,6 +30,7 @@
     flex-direction: column;
     align-items: center;
     justify-items: start;
+    overflow: visible;
 
     [data-slot="dialog-content"] {
       display: flex;
@@ -39,6 +40,14 @@
       width: 100%;
       max-height: 100%;
       min-height: 280px;
+      overflow: auto;
+
+      /* Hide scrollbar */
+      scrollbar-width: none;
+      -ms-overflow-style: none;
+      &::-webkit-scrollbar {
+        display: none;
+      }
 
       /* padding: 8px; */
       /* padding: 8px 8px 0 8px; */
@@ -108,7 +117,7 @@
         display: flex;
         flex-direction: column;
         flex: 1;
-        overflow-y: auto;
+        overflow: hidden;
 
         &:focus-visible {
           outline: none;
@@ -129,6 +138,11 @@
       }
     }
   }
+
+  &[data-size="large"] [data-slot="dialog-container"] {
+    width: min(calc(100vw - 32px), 800px);
+    height: min(calc(100vh - 32px), 600px);
+  }
 }
 
 @keyframes overlayShow {

+ 3 - 1
packages/ui/src/components/dialog.tsx

@@ -6,6 +6,7 @@ export interface DialogProps extends ParentProps {
   title?: JSXElement
   description?: JSXElement
   action?: JSXElement
+  size?: "normal" | "large"
   class?: ComponentProps<"div">["class"]
   classList?: ComponentProps<"div">["classList"]
   fit?: boolean
@@ -13,10 +14,11 @@ export interface DialogProps extends ParentProps {
 
 export function Dialog(props: DialogProps) {
   return (
-    <div data-component="dialog" data-fit={props.fit ? true : undefined}>
+    <div data-component="dialog" data-fit={props.fit ? true : undefined} data-size={props.size || "normal"}>
       <div data-slot="dialog-container">
         <Kobalte.Content
           data-slot="dialog-content"
+          data-no-header={!props.title && !props.action ? "" : undefined}
           classList={{
             ...(props.classList ?? {}),
             [props.class ?? ""]: !!props.class,

+ 101 - 21
packages/ui/src/components/tabs.css

@@ -215,24 +215,36 @@
       height: 100%;
       overflow-x: hidden;
       overflow-y: auto;
+      padding: 8px;
+      gap: 4px;
+      background-color: var(--background-base);
+      border-right: 1px solid var(--border-weak-base);
 
       &::after {
-        width: 100%;
-        height: auto;
-        flex-grow: 1;
-        border-bottom: none;
-        border-right: 1px solid var(--border-weak-base);
+        display: none;
       }
     }
 
     [data-slot="tabs-trigger-wrapper"] {
       width: 100%;
-      height: auto;
-      border-bottom: none;
-      border-right: 1px solid var(--border-weak-base);
+      height: 32px;
+      border: none;
+      border-radius: 8px;
+      background-color: transparent;
+
+      [data-slot="tabs-trigger"] {
+        padding: 0 8px;
+        gap: 8px;
+        justify-content: flex-start;
+      }
+
+      &:hover:not(:disabled) {
+        background-color: var(--surface-raised-base-hover);
+      }
 
       &:has([data-selected]) {
-        border-right-color: transparent;
+        background-color: var(--surface-raised-base-hover);
+        color: var(--text-strong);
       }
     }
 
@@ -243,32 +255,100 @@
 
     &[data-variant="alt"] {
       [data-slot="tabs-list"] {
-        padding-left: 0;
-        padding-right: 0;
-        padding-top: 24px;
-        padding-bottom: 24px;
-        border-bottom: none;
-        border-right: 1px solid var(--border-weak-base);
+        padding: 8px;
+        gap: 4px;
+        border: none;
 
         &::after {
+          display: none;
+        }
+      }
+
+      [data-slot="tabs-trigger-wrapper"] {
+        height: 32px;
+        border: none;
+        border-radius: 8px;
+
+        [data-slot="tabs-trigger"] {
           border: none;
+          padding: 0 8px;
+          gap: 8px;
+          justify-content: flex-start;
+        }
+
+        &:hover:not(:disabled) {
+          background-color: var(--surface-raised-base-hover);
+        }
+
+        &:has([data-selected]) {
+          background-color: var(--surface-raised-base-hover);
+          color: var(--text-strong);
+        }
+      }
+    }
+
+    &[data-variant="settings"] {
+      [data-slot="tabs-list"] {
+        width: 180px;
+        min-width: 180px;
+        padding: 12px;
+        gap: 0;
+        background-color: var(--background-base);
+        border-right: 1px solid var(--border-weak-base);
+
+        &::after {
+          display: none;
         }
       }
 
+      [data-slot="tabs-section-title"] {
+        padding: 8px 8px 4px 8px;
+        font-family: var(--font-family-sans);
+        font-size: var(--font-size-small);
+        font-weight: var(--font-weight-medium);
+        color: var(--text-weak);
+      }
+
       [data-slot="tabs-trigger-wrapper"] {
-        border-bottom: none;
-        border-right-width: 2px;
-        border-right-style: solid;
-        border-right-color: transparent;
+        height: 32px;
+        border: none;
+        border-radius: var(--radius-md);
+
+        /* text-14-regular */
+        font-family: var(--font-family-sans);
+        font-size: var(--font-size-base);
+        font-weight: var(--font-weight-regular);
+        line-height: var(--line-height-large);
 
         [data-slot="tabs-trigger"] {
-          border-bottom: none;
+          border: none;
+          padding: 0 8px;
+          gap: 8px;
+          justify-content: flex-start;
+          width: 100%;
+        }
+
+        [data-component="icon"] {
+          color: var(--icon-base);
+        }
+
+        &:hover:not(:disabled) {
+          background-color: var(--surface-raised-base-hover);
         }
 
         &:has([data-selected]) {
-          border-right-color: var(--icon-strong-base);
+          background-color: var(--surface-raised-base-hover);
+          color: var(--text-strong);
+
+          [data-component="icon"] {
+            color: var(--icon-strong-base);
+          }
         }
       }
+
+      [data-slot="tabs-content"] {
+        background-color: var(--surface-raised-stronger-non-alpha);
+      }
     }
   }
 }

+ 7 - 2
packages/ui/src/components/tabs.tsx

@@ -1,9 +1,9 @@
 import { Tabs as Kobalte } from "@kobalte/core/tabs"
 import { Show, splitProps, type JSX } from "solid-js"
-import type { ComponentProps, ParentProps } from "solid-js"
+import type { ComponentProps, ParentProps, Component } from "solid-js"
 
 export interface TabsProps extends ComponentProps<typeof Kobalte> {
-  variant?: "normal" | "alt"
+  variant?: "normal" | "alt" | "settings"
   orientation?: "horizontal" | "vertical"
 }
 export interface TabsListProps extends ComponentProps<typeof Kobalte.List> {}
@@ -106,8 +106,13 @@ function TabsContent(props: ParentProps<TabsContentProps>) {
   )
 }
 
+const TabsSectionTitle: Component<ParentProps> = (props) => {
+  return <div data-slot="tabs-section-title">{props.children}</div>
+}
+
 export const Tabs = Object.assign(TabsRoot, {
   List: TabsList,
   Trigger: TabsTrigger,
   Content: TabsContent,
+  SectionTitle: TabsSectionTitle,
 })