Compare commits

...

7 Commits

Author SHA1 Message Date
Jamison Lahman
77ce667b21 nit 2026-04-05 18:45:22 -07:00
Jamison Lahman
1e0a8afc72 INTERNAL_URL 2026-04-05 18:05:02 -07:00
Jamison Lahman
85302a1cde feat(cli): --config-file and --server-url 2026-04-05 17:32:48 -07:00
Raunak Bhagat
abc2cd5572 refactor: flatten opal card layouts, add children to CardHeaderLayout (#9907) 2026-04-04 02:50:55 +00:00
Raunak Bhagat
a704acbf73 fix: Edit AccountPopover + Separator's appearances when folded (#9906) 2026-04-04 01:24:59 +00:00
Jamison Lahman
8737122133 Revert "chore(deps): bump litellm from 1.81.6 to 1.83.0 (#9898)" (#9908) 2026-04-03 18:06:54 -07:00
Raunak Bhagat
c5d7cfa896 refactor: rework admin sidebar footer (#9895) 2026-04-04 00:08:42 +00:00
31 changed files with 380 additions and 295 deletions

View File

@@ -461,7 +461,7 @@ lazy-imports==1.0.1
# via onyx
legacy-cgi==2.6.4 ; python_full_version >= '3.13'
# via ddtrace
litellm==1.83.0
litellm==1.81.6
# via onyx
locket==1.0.0
# via

View File

@@ -219,7 +219,7 @@ kiwisolver==1.4.9
# via matplotlib
kubernetes==31.0.0
# via onyx
litellm==1.83.0
litellm==1.81.6
# via onyx
mako==1.2.4
# via alembic

View File

@@ -154,7 +154,7 @@ jsonschema-specifications==2025.9.1
# via jsonschema
kubernetes==31.0.0
# via onyx
litellm==1.83.0
litellm==1.81.6
# via onyx
markupsafe==3.0.3
# via jinja2

View File

@@ -189,7 +189,7 @@ kombu==5.5.4
# via celery
kubernetes==31.0.0
# via onyx
litellm==1.83.0
litellm==1.81.6
# via onyx
markupsafe==3.0.3
# via jinja2

View File

@@ -6,7 +6,6 @@ import (
"text/tabwriter"
"github.com/onyx-dot-app/onyx/cli/internal/api"
"github.com/onyx-dot-app/onyx/cli/internal/config"
"github.com/onyx-dot-app/onyx/cli/internal/exitcodes"
"github.com/spf13/cobra"
)
@@ -25,7 +24,7 @@ Use --json for machine-readable output.`,
onyx-cli agents --json
onyx-cli agents --json | jq '.[].name'`,
RunE: func(cmd *cobra.Command, args []string) error {
cfg := config.Load()
cfg := loadConfig(cmd)
if !cfg.IsConfigured() {
return exitcodes.New(exitcodes.NotConfigured, "onyx CLI is not configured\n Run: onyx-cli configure")
}

View File

@@ -11,7 +11,6 @@ import (
"syscall"
"github.com/onyx-dot-app/onyx/cli/internal/api"
"github.com/onyx-dot-app/onyx/cli/internal/config"
"github.com/onyx-dot-app/onyx/cli/internal/exitcodes"
"github.com/onyx-dot-app/onyx/cli/internal/models"
"github.com/onyx-dot-app/onyx/cli/internal/overflow"
@@ -49,7 +48,7 @@ to a temp file. Set --max-output 0 to disable truncation.`,
cat error.log | onyx-cli ask --prompt "Find the root cause"
echo "what is onyx?" | onyx-cli ask`,
RunE: func(cmd *cobra.Command, args []string) error {
cfg := config.Load()
cfg := loadConfig(cmd)
if !cfg.IsConfigured() {
return exitcodes.New(exitcodes.NotConfigured, "onyx CLI is not configured\n Run: onyx-cli configure")
}

View File

@@ -21,7 +21,7 @@ an interactive setup wizard will guide you through configuration.`,
Example: ` onyx-cli chat
onyx-cli`,
RunE: func(cmd *cobra.Command, args []string) error {
cfg := config.Load()
cfg := loadConfig(cmd)
// First-run: onboarding
if !config.ConfigExists() || !cfg.IsConfigured() {

View File

@@ -69,7 +69,7 @@ Use --dry-run to test the connection without saving the configuration.`,
return exitcodes.New(exitcodes.BadRequest, "both --server-url and --api-key are required for non-interactive setup\n Run 'onyx-cli configure' without flags for interactive setup")
}
cfg := config.Load()
cfg := loadConfig(cmd)
onboarding.Run(&cfg)
return nil
},

View File

@@ -12,7 +12,7 @@ func newExperimentsCmd() *cobra.Command {
Use: "experiments",
Short: "List experimental features and their status",
RunE: func(cmd *cobra.Command, args []string) error {
cfg := config.Load()
cfg := loadConfig(cmd)
_, _ = fmt.Fprintln(cmd.OutOrStdout(), config.ExperimentsText(cfg.Features))
return nil
},

View File

@@ -13,6 +13,20 @@ import (
"github.com/spf13/cobra"
)
// loadConfig loads the CLI config, using the --config-file persistent flag if set.
func loadConfig(cmd *cobra.Command) config.OnyxCliConfig {
cf, _ := cmd.Flags().GetString("config-file")
return config.Load(cf)
}
// effectiveConfigPath returns the config file path, respecting --config-file.
func effectiveConfigPath(cmd *cobra.Command) string {
if cf, _ := cmd.Flags().GetString("config-file"); cf != "" {
return cf
}
return config.ConfigFilePath()
}
// Version and Commit are set via ldflags at build time.
var (
Version string
@@ -29,7 +43,7 @@ func fullVersion() string {
func printVersion(cmd *cobra.Command) {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Client version: %s\n", fullVersion())
cfg := config.Load()
cfg := loadConfig(cmd)
if !cfg.IsConfigured() {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Server version: unknown (not configured)\n")
return
@@ -84,6 +98,8 @@ func Execute() error {
}
rootCmd.PersistentFlags().BoolVar(&opts.Debug, "debug", false, "run in debug mode")
rootCmd.PersistentFlags().String("config-file", "",
"Path to config file (default: "+config.ConfigFilePath()+")")
// Custom --version flag instead of Cobra's built-in (which only shows one version string)
var showVersion bool

View File

@@ -50,16 +50,14 @@ func sessionEnv(s ssh.Session, key string) string {
return ""
}
func validateAPIKey(serverURL string, apiKey string) error {
func validateAPIKey(serverCfg config.OnyxCliConfig, apiKey string) error {
trimmedKey := strings.TrimSpace(apiKey)
if len(trimmedKey) > maxAPIKeyLength {
return fmt.Errorf("API key is too long (max %d characters)", maxAPIKeyLength)
}
cfg := config.OnyxCliConfig{
ServerURL: serverURL,
APIKey: trimmedKey,
}
cfg := serverCfg
cfg.APIKey = trimmedKey
client := api.NewClient(cfg)
ctx, cancel := context.WithTimeout(context.Background(), apiKeyValidationTimeout)
defer cancel()
@@ -83,7 +81,7 @@ type authValidatedMsg struct {
type authModel struct {
input textinput.Model
serverURL string
serverCfg config.OnyxCliConfig
state authState
apiKey string // set on successful validation
errMsg string
@@ -91,7 +89,7 @@ type authModel struct {
aborted bool
}
func newAuthModel(serverURL, initialErr string) authModel {
func newAuthModel(serverCfg config.OnyxCliConfig, initialErr string) authModel {
ti := textinput.New()
ti.Prompt = " API Key: "
ti.EchoMode = textinput.EchoPassword
@@ -102,7 +100,7 @@ func newAuthModel(serverURL, initialErr string) authModel {
return authModel{
input: ti,
serverURL: serverURL,
serverCfg: serverCfg,
errMsg: initialErr,
}
}
@@ -138,9 +136,9 @@ func (m authModel) Update(msg tea.Msg) (authModel, tea.Cmd) {
}
m.state = authValidating
m.errMsg = ""
serverURL := m.serverURL
serverCfg := m.serverCfg
return m, func() tea.Msg {
return authValidatedMsg{key: key, err: validateAPIKey(serverURL, key)}
return authValidatedMsg{key: key, err: validateAPIKey(serverCfg, key)}
}
}
@@ -171,12 +169,13 @@ func (m authModel) Update(msg tea.Msg) (authModel, tea.Cmd) {
}
func (m authModel) View() string {
settingsURL := strings.TrimRight(m.serverURL, "/") + "/app/settings/accounts-access"
serverURL := m.serverCfg.ServerURL
settingsURL := strings.TrimRight(serverURL, "/") + "/app/settings/accounts-access"
var b strings.Builder
b.WriteString("\n")
b.WriteString(" \x1b[1;35mOnyx CLI\x1b[0m\n")
b.WriteString(" \x1b[90m" + m.serverURL + "\x1b[0m\n")
b.WriteString(" \x1b[90m" + serverURL + "\x1b[0m\n")
b.WriteString("\n")
b.WriteString(" Generate an API key at:\n")
b.WriteString(" \x1b[4;34m" + settingsURL + "\x1b[0m\n")
@@ -215,7 +214,7 @@ type serveModel struct {
func newServeModel(serverCfg config.OnyxCliConfig, initialErr string) serveModel {
return serveModel{
auth: newAuthModel(serverCfg.ServerURL, initialErr),
auth: newAuthModel(serverCfg, initialErr),
serverCfg: serverCfg,
}
}
@@ -238,11 +237,8 @@ func (m serveModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, tea.Quit
}
if m.auth.apiKey != "" {
cfg := config.OnyxCliConfig{
ServerURL: m.serverCfg.ServerURL,
APIKey: m.auth.apiKey,
DefaultAgentID: m.serverCfg.DefaultAgentID,
}
cfg := m.serverCfg
cfg.APIKey = m.auth.apiKey
m.tui = tui.NewModel(cfg)
m.authed = true
w, h := m.width, m.height
@@ -280,6 +276,8 @@ func newServeCmd() *cobra.Command {
rateLimitPerMin int
rateLimitBurst int
rateLimitCache int
serverURL string
apiServerURL string
)
cmd := &cobra.Command{
@@ -300,9 +298,18 @@ environment variable (the --host-key flag takes precedence).`,
Example: ` onyx-cli serve --port 2222
ssh localhost -p 2222
onyx-cli serve --host 0.0.0.0 --port 2222
onyx-cli serve --idle-timeout 30m --max-session-timeout 2h`,
onyx-cli serve --idle-timeout 30m --max-session-timeout 2h
onyx-cli serve --server-url https://my-onyx.example.com
onyx-cli serve --api-server-url http://api_server:8080 # bypass nginx
onyx-cli serve --config-file /etc/onyx-cli/config.json # global flag`,
RunE: func(cmd *cobra.Command, args []string) error {
serverCfg := config.Load()
serverCfg := loadConfig(cmd)
if cmd.Flags().Changed("server-url") {
serverCfg.ServerURL = serverURL
}
if cmd.Flags().Changed("api-server-url") {
serverCfg.InternalURL = apiServerURL
}
if serverCfg.ServerURL == "" {
return exitcodes.New(exitcodes.NotConfigured, "server URL is not configured\n Run: onyx-cli configure")
}
@@ -333,7 +340,7 @@ environment variable (the --host-key flag takes precedence).`,
var envErr string
if apiKey != "" {
if err := validateAPIKey(serverCfg.ServerURL, apiKey); err != nil {
if err := validateAPIKey(serverCfg, apiKey); err != nil {
envErr = fmt.Sprintf("ONYX_API_KEY from SSH environment is invalid: %s", err.Error())
apiKey = ""
}
@@ -341,11 +348,8 @@ environment variable (the --host-key flag takes precedence).`,
if apiKey != "" {
// Env key is valid — go straight to the TUI.
cfg := config.OnyxCliConfig{
ServerURL: serverCfg.ServerURL,
APIKey: apiKey,
DefaultAgentID: serverCfg.DefaultAgentID,
}
cfg := serverCfg
cfg.APIKey = apiKey
return tui.NewModel(cfg), []tea.ProgramOption{
tea.WithAltScreen(),
tea.WithMouseCellMotion(),
@@ -446,6 +450,10 @@ environment variable (the --host-key flag takes precedence).`,
defaultServeRateLimitCacheSize,
"Maximum number of IP limiter entries tracked in memory",
)
cmd.Flags().StringVar(&serverURL, "server-url", "",
"Onyx server URL (overrides config file and $"+config.EnvServerURL+")")
cmd.Flags().StringVar(&apiServerURL, "api-server-url", "",
"API server URL for direct access, bypassing nginx (overrides $"+config.EnvAPIServerURL+")")
return cmd
}

View File

@@ -4,10 +4,10 @@ import (
"context"
"errors"
"fmt"
"os"
"time"
"github.com/onyx-dot-app/onyx/cli/internal/api"
"github.com/onyx-dot-app/onyx/cli/internal/config"
"github.com/onyx-dot-app/onyx/cli/internal/exitcodes"
"github.com/onyx-dot-app/onyx/cli/internal/version"
log "github.com/sirupsen/logrus"
@@ -23,19 +23,21 @@ is valid. Also reports the server version and warns if it is below the
minimum required.`,
Example: ` onyx-cli validate-config`,
RunE: func(cmd *cobra.Command, args []string) error {
cfgPath := effectiveConfigPath(cmd)
// Check config file
if !config.ConfigExists() {
return exitcodes.Newf(exitcodes.NotConfigured, "config file not found at %s\n Run: onyx-cli configure", config.ConfigFilePath())
if _, err := os.Stat(cfgPath); err != nil {
return exitcodes.Newf(exitcodes.NotConfigured, "config file not found at %s\n Run: onyx-cli configure", cfgPath)
}
cfg := config.Load()
cfg := loadConfig(cmd)
// Check API key
if !cfg.IsConfigured() {
return exitcodes.New(exitcodes.NotConfigured, "API key is missing\n Run: onyx-cli configure")
}
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Config: %s\n", config.ConfigFilePath())
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Config: %s\n", cfgPath)
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Server: %s\n", cfg.ServerURL)
// Test connection

View File

@@ -16,18 +16,30 @@ import (
"github.com/onyx-dot-app/onyx/cli/internal/config"
"github.com/onyx-dot-app/onyx/cli/internal/models"
log "github.com/sirupsen/logrus"
)
// Client is the Onyx API client.
type Client struct {
baseURL string
serverURL string // root server URL (for reachability checks)
baseURL string // API base URL (includes /api when going through nginx)
apiKey string
httpClient *http.Client // default 30s timeout for quick requests
longHTTPClient *http.Client // 5min timeout for streaming/uploads
}
// NewClient creates a new API client from config.
//
// When InternalURL is set, requests go directly to the API server (no /api
// prefix needed — mirrors INTERNAL_URL in the web server). Otherwise,
// requests go through the nginx proxy at ServerURL which strips /api.
func NewClient(cfg config.OnyxCliConfig) *Client {
baseURL := apiBaseURL(cfg)
log.WithFields(log.Fields{
"server_url": cfg.ServerURL,
"internal_url": cfg.InternalURL,
"base_url": baseURL,
}).Debug("creating API client")
var transport *http.Transport
if t, ok := http.DefaultTransport.(*http.Transport); ok {
transport = t.Clone()
@@ -35,8 +47,9 @@ func NewClient(cfg config.OnyxCliConfig) *Client {
transport = &http.Transport{}
}
return &Client{
baseURL: strings.TrimRight(cfg.ServerURL, "/"),
apiKey: cfg.APIKey,
serverURL: strings.TrimRight(cfg.ServerURL, "/"),
baseURL: baseURL,
apiKey: cfg.APIKey,
httpClient: &http.Client{
Timeout: 30 * time.Second,
Transport: transport,
@@ -48,14 +61,27 @@ func NewClient(cfg config.OnyxCliConfig) *Client {
}
}
// apiBaseURL returns the base URL for API requests. When InternalURL is set,
// it points directly at the API server. Otherwise it goes through the nginx
// proxy at ServerURL/api.
func apiBaseURL(cfg config.OnyxCliConfig) string {
if cfg.InternalURL != "" {
return strings.TrimRight(cfg.InternalURL, "/")
}
return strings.TrimRight(cfg.ServerURL, "/") + "/api"
}
// UpdateConfig replaces the client's config.
func (c *Client) UpdateConfig(cfg config.OnyxCliConfig) {
c.baseURL = strings.TrimRight(cfg.ServerURL, "/")
c.serverURL = strings.TrimRight(cfg.ServerURL, "/")
c.baseURL = apiBaseURL(cfg)
c.apiKey = cfg.APIKey
}
func (c *Client) newRequest(ctx context.Context, method, path string, body io.Reader) (*http.Request, error) {
req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, body)
url := c.baseURL + path
log.WithFields(log.Fields{"method": method, "url": url}).Debug("API request")
req, err := http.NewRequestWithContext(ctx, method, url, body)
if err != nil {
return nil, err
}
@@ -87,12 +113,16 @@ func (c *Client) doJSON(ctx context.Context, method, path string, reqBody any, r
resp, err := c.httpClient.Do(req)
if err != nil {
log.WithError(err).WithField("url", req.URL.String()).Debug("API request failed")
return err
}
defer func() { _ = resp.Body.Close() }()
log.WithFields(log.Fields{"url": req.URL.String(), "status": resp.StatusCode}).Debug("API response")
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
respBody, _ := io.ReadAll(resp.Body)
log.WithFields(log.Fields{"status": resp.StatusCode, "body": string(respBody)}).Debug("API error response")
return &OnyxAPIError{StatusCode: resp.StatusCode, Detail: string(respBody)}
}
@@ -105,16 +135,26 @@ func (c *Client) doJSON(ctx context.Context, method, path string, reqBody any, r
// TestConnection checks if the server is reachable and credentials are valid.
// Returns nil on success, or an error with a descriptive message on failure.
func (c *Client) TestConnection(ctx context.Context) error {
// Step 1: Basic reachability
req, err := c.newRequest(ctx, "GET", "/", nil)
// Step 1: Basic reachability (hit the server root, not the API prefix)
reachURL := c.serverURL
if reachURL == "" {
reachURL = c.baseURL
}
log.WithFields(log.Fields{
"reach_url": reachURL,
"base_url": c.baseURL,
}).Debug("testing connection — step 1: reachability")
req, err := http.NewRequestWithContext(ctx, "GET", reachURL+"/", nil)
if err != nil {
return fmt.Errorf("cannot connect to %s: %w", c.baseURL, err)
return fmt.Errorf("cannot connect to %s: %w", reachURL, err)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("cannot connect to %s — is the server running?", c.baseURL)
log.WithError(err).Debug("reachability check failed")
return fmt.Errorf("cannot connect to %s — is the server running?", reachURL)
}
log.WithField("status", resp.StatusCode).Debug("reachability check response")
_ = resp.Body.Close()
serverHeader := strings.ToLower(resp.Header.Get("Server"))
@@ -127,7 +167,8 @@ func (c *Client) TestConnection(ctx context.Context) error {
}
// Step 2: Authenticated check
req2, err := c.newRequest(ctx, "GET", "/api/me", nil)
log.WithField("url", c.baseURL+"/me").Debug("testing connection — step 2: auth check")
req2, err := c.newRequest(ctx, "GET", "/me", nil)
if err != nil {
return fmt.Errorf("server reachable but API error: %w", err)
}
@@ -167,7 +208,7 @@ func (c *Client) TestConnection(ctx context.Context) error {
// ListAgents returns visible agents.
func (c *Client) ListAgents(ctx context.Context) ([]models.AgentSummary, error) {
var raw []models.AgentSummary
if err := c.doJSON(ctx, "GET", "/api/persona", nil, &raw); err != nil {
if err := c.doJSON(ctx, "GET", "/persona", nil, &raw); err != nil {
return nil, err
}
var result []models.AgentSummary
@@ -184,7 +225,7 @@ func (c *Client) ListChatSessions(ctx context.Context) ([]models.ChatSessionDeta
var resp struct {
Sessions []models.ChatSessionDetails `json:"sessions"`
}
if err := c.doJSON(ctx, "GET", "/api/chat/get-user-chat-sessions", nil, &resp); err != nil {
if err := c.doJSON(ctx, "GET", "/chat/get-user-chat-sessions", nil, &resp); err != nil {
return nil, err
}
return resp.Sessions, nil
@@ -193,7 +234,7 @@ func (c *Client) ListChatSessions(ctx context.Context) ([]models.ChatSessionDeta
// GetChatSession returns full details for a session.
func (c *Client) GetChatSession(ctx context.Context, sessionID string) (*models.ChatSessionDetailResponse, error) {
var resp models.ChatSessionDetailResponse
if err := c.doJSON(ctx, "GET", "/api/chat/get-chat-session/"+sessionID, nil, &resp); err != nil {
if err := c.doJSON(ctx, "GET", "/chat/get-chat-session/"+sessionID, nil, &resp); err != nil {
return nil, err
}
return &resp, nil
@@ -210,7 +251,7 @@ func (c *Client) RenameChatSession(ctx context.Context, sessionID string, name *
var resp struct {
NewName string `json:"new_name"`
}
if err := c.doJSON(ctx, "PUT", "/api/chat/rename-chat-session", payload, &resp); err != nil {
if err := c.doJSON(ctx, "PUT", "/chat/rename-chat-session", payload, &resp); err != nil {
return "", err
}
return resp.NewName, nil
@@ -236,7 +277,7 @@ func (c *Client) UploadFile(ctx context.Context, filePath string) (*models.FileD
}
_ = writer.Close()
req, err := c.newRequest(ctx, "POST", "/api/user/projects/file/upload", &buf)
req, err := c.newRequest(ctx, "POST", "/user/projects/file/upload", &buf)
if err != nil {
return nil, err
}
@@ -275,7 +316,7 @@ func (c *Client) GetBackendVersion(ctx context.Context) (string, error) {
var resp struct {
BackendVersion string `json:"backend_version"`
}
if err := c.doJSON(ctx, "GET", "/api/version", nil, &resp); err != nil {
if err := c.doJSON(ctx, "GET", "/version", nil, &resp); err != nil {
return "", err
}
return resp.BackendVersion, nil
@@ -283,7 +324,7 @@ func (c *Client) GetBackendVersion(ctx context.Context) (string, error) {
// StopChatSession sends a stop signal for a streaming session (best-effort).
func (c *Client) StopChatSession(ctx context.Context, sessionID string) {
req, err := c.newRequest(ctx, "POST", "/api/chat/stop-chat-session/"+sessionID, nil)
req, err := c.newRequest(ctx, "POST", "/chat/stop-chat-session/"+sessionID, nil)
if err != nil {
return
}

View File

@@ -64,7 +64,7 @@ func (c *Client) SendMessageStream(
return
}
req, err := http.NewRequestWithContext(ctx, "POST", c.baseURL+"/api/chat/send-chat-message", nil)
req, err := http.NewRequestWithContext(ctx, "POST", c.baseURL+"/chat/send-chat-message", nil)
if err != nil {
ch <- models.ErrorEvent{Error: fmt.Sprintf("request error: %v", err), IsRetryable: false}
return

View File

@@ -10,6 +10,7 @@ import (
const (
EnvServerURL = "ONYX_SERVER_URL"
EnvAPIServerURL = "ONYX_API_SERVER_URL"
EnvAPIKey = "ONYX_API_KEY"
EnvAgentID = "ONYX_PERSONA_ID"
EnvSSHHostKey = "ONYX_SSH_HOST_KEY"
@@ -27,6 +28,7 @@ type Features struct {
// OnyxCliConfig holds the CLI configuration.
type OnyxCliConfig struct {
ServerURL string `json:"server_url"`
InternalURL string `json:"internal_url,omitempty"`
APIKey string `json:"api_key"`
DefaultAgentID int `json:"default_persona_id"`
Features Features `json:"features,omitempty"`
@@ -78,30 +80,47 @@ func ConfigExists() bool {
return err == nil
}
// LoadFromDisk reads config from the file only, without applying environment
// variable overrides. Use this when you need the persisted config values
// (e.g., to preserve them during a save operation).
func LoadFromDisk() OnyxCliConfig {
// LoadFromDisk reads config from the given file path without applying
// environment variable overrides. Use this when you need the persisted
// config values (e.g., to preserve them during a save operation).
// If no path is provided, the default config file path is used.
func LoadFromDisk(path ...string) OnyxCliConfig {
p := ConfigFilePath()
if len(path) > 0 && path[0] != "" {
p = path[0]
}
cfg := DefaultConfig()
data, err := os.ReadFile(ConfigFilePath())
data, err := os.ReadFile(p)
if err == nil {
if jsonErr := json.Unmarshal(data, &cfg); jsonErr != nil {
fmt.Fprintf(os.Stderr, "warning: config file %s is malformed: %v (using defaults)\n", ConfigFilePath(), jsonErr)
fmt.Fprintf(os.Stderr, "warning: config file %s is malformed: %v (using defaults)\n", p, jsonErr)
}
}
return cfg
}
// Load reads config from file and applies environment variable overrides.
func Load() OnyxCliConfig {
cfg := LoadFromDisk()
// Load reads config from the given file path and applies environment variable
// overrides. If no path is provided, the default config file path is used.
func Load(path ...string) OnyxCliConfig {
cfg := LoadFromDisk(path...)
applyEnvOverrides(&cfg)
return cfg
}
// Environment overrides
func applyEnvOverrides(cfg *OnyxCliConfig) {
if v := os.Getenv(EnvServerURL); v != "" {
cfg.ServerURL = v
}
// ONYX_API_SERVER_URL takes precedence; fall back to INTERNAL_URL
// (the env var used by the web server) for compatibility.
if v := os.Getenv(EnvAPIServerURL); v != "" {
cfg.InternalURL = v
} else if v := os.Getenv("INTERNAL_URL"); v != "" {
cfg.InternalURL = v
}
if v := os.Getenv(EnvAPIKey); v != "" {
cfg.APIKey = v
}
@@ -117,8 +136,6 @@ func Load() OnyxCliConfig {
fmt.Fprintf(os.Stderr, "warning: invalid value %q for %s (expected true/false), ignoring\n", v, EnvStreamMarkdown)
}
}
return cfg
}
// Save writes the config to disk, creating parent directories if needed.

View File

@@ -12,7 +12,7 @@ dependencies = [
"cohere==5.6.1",
"fastapi==0.133.1",
"google-genai==1.52.0",
"litellm==1.83.0",
"litellm==1.81.6",
"openai==2.14.0",
"pydantic==2.11.7",
"prometheus_client>=0.21.1",

8
uv.lock generated
View File

@@ -3134,7 +3134,7 @@ wheels = [
[[package]]
name = "litellm"
version = "1.83.0"
version = "1.81.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohttp" },
@@ -3150,9 +3150,9 @@ dependencies = [
{ name = "tiktoken" },
{ name = "tokenizers" },
]
sdist = { url = "https://files.pythonhosted.org/packages/22/92/6ce9737554994ca8e536e5f4f6a87cc7c4774b656c9eb9add071caf7d54b/litellm-1.83.0.tar.gz", hash = "sha256:860bebc76c4bb27b4cf90b4a77acd66dba25aced37e3db98750de8a1766bfb7a", size = 17333062, upload-time = "2026-03-31T05:08:25.331Z" }
sdist = { url = "https://files.pythonhosted.org/packages/2e/f3/194a2dca6cb3eddb89f4bc2920cf5e27542256af907c23be13c61fe7e021/litellm-1.81.6.tar.gz", hash = "sha256:f02b503dfb7d66d1c939f82e4db21aeec1d6e2ed1fe3f5cd02aaec3f792bc4ae", size = 13878107, upload-time = "2026-02-01T04:02:27.36Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/19/2c/a670cc050fcd6f45c6199eb99e259c73aea92edba8d5c2fc1b3686d36217/litellm-1.83.0-py3-none-any.whl", hash = "sha256:88c536d339248f3987571493015784671ba3f193a328e1ea6780dbebaa2094a8", size = 15610306, upload-time = "2026-03-31T05:08:21.987Z" },
{ url = "https://files.pythonhosted.org/packages/e6/05/3516cc7386b220d388aa0bd833308c677e94eceb82b2756dd95e06f6a13f/litellm-1.81.6-py3-none-any.whl", hash = "sha256:573206ba194d49a1691370ba33f781671609ac77c35347f8a0411d852cf6341a", size = 12224343, upload-time = "2026-02-01T04:02:23.704Z" },
]
[[package]]
@@ -4443,7 +4443,7 @@ requires-dist = [
{ name = "langchain-core", marker = "extra == 'backend'", specifier = "==1.2.22" },
{ name = "langfuse", marker = "extra == 'backend'", specifier = "==3.10.0" },
{ name = "lazy-imports", marker = "extra == 'backend'", specifier = "==1.0.1" },
{ name = "litellm", specifier = "==1.83.0" },
{ name = "litellm", specifier = "==1.81.6" },
{ name = "lxml", marker = "extra == 'backend'", specifier = "==5.3.0" },
{ name = "mako", marker = "extra == 'backend'", specifier = "==1.2.4" },
{ name = "manygo", marker = "extra == 'dev'", specifier = "==0.2.0" },

View File

@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react";
import { CardHeaderLayout } from "@opal/layouts";
import { Card } from "@opal/layouts";
import { Button } from "@opal/components";
import {
SvgArrowExchange,
@@ -18,14 +18,14 @@ const withTooltipProvider: Decorator = (Story) => (
);
const meta = {
title: "Layouts/CardHeaderLayout",
component: CardHeaderLayout,
title: "Layouts/Card.Header",
component: Card.Header,
tags: ["autodocs"],
decorators: [withTooltipProvider],
parameters: {
layout: "centered",
},
} satisfies Meta<typeof CardHeaderLayout>;
} satisfies Meta<typeof Card.Header>;
export default meta;
@@ -38,7 +38,7 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: () => (
<div className="w-[28rem] border rounded-16">
<CardHeaderLayout
<Card.Header
sizePreset="main-ui"
variant="section"
icon={SvgGlobe}
@@ -57,7 +57,7 @@ export const Default: Story = {
export const WithBothSlots: Story = {
render: () => (
<div className="w-[28rem] border rounded-16">
<CardHeaderLayout
<Card.Header
sizePreset="main-ui"
variant="section"
icon={SvgGlobe}
@@ -92,7 +92,7 @@ export const WithBothSlots: Story = {
export const RightChildrenOnly: Story = {
render: () => (
<div className="w-[28rem] border rounded-16">
<CardHeaderLayout
<Card.Header
sizePreset="main-ui"
variant="section"
icon={SvgGlobe}
@@ -111,7 +111,7 @@ export const RightChildrenOnly: Story = {
export const NoRightChildren: Story = {
render: () => (
<div className="w-[28rem] border rounded-16">
<CardHeaderLayout
<Card.Header
sizePreset="main-ui"
variant="section"
icon={SvgGlobe}
@@ -125,7 +125,7 @@ export const NoRightChildren: Story = {
export const LongContent: Story = {
render: () => (
<div className="w-[28rem] border rounded-16">
<CardHeaderLayout
<Card.Header
sizePreset="main-ui"
variant="section"
icon={SvgGlobe}

View File

@@ -0,0 +1,116 @@
# Card
**Import:** `import { Card } from "@opal/layouts";`
A namespace of card layout primitives. Each sub-component handles a specific region of a card.
## Card.Header
A card header layout that pairs a [`Content`](../content/README.md) block with a right-side column and an optional full-width children slot.
### Why Card.Header?
[`ContentAction`](../content-action/README.md) provides a single `rightChildren` slot. Card headers typically need two distinct right-side regions — a primary action on top and secondary actions on the bottom. `Card.Header` provides this with `rightChildren` and `bottomRightChildren` slots, plus a `children` slot for full-width content below the header row (e.g., search bars, expandable tool lists).
### Props
Inherits **all** props from [`Content`](../content/README.md) (icon, title, description, sizePreset, variant, editable, onTitleChange, suffix, etc.) plus:
| Prop | Type | Default | Description |
|---|---|---|---|
| `rightChildren` | `ReactNode` | `undefined` | Content rendered to the right of the Content block (top of right column). |
| `bottomRightChildren` | `ReactNode` | `undefined` | Content rendered below `rightChildren` in the same column. Laid out as `flex flex-row`. |
| `children` | `ReactNode` | `undefined` | Content rendered below the full header row, spanning the entire width. |
### Layout Structure
```
+---------------------------------------------------------+
| [Content (p-2, self-start)] [rightChildren] |
| icon + title + description [bottomRightChildren] |
+---------------------------------------------------------+
| [children — full width] |
+---------------------------------------------------------+
```
- Outer wrapper: `flex flex-col w-full`
- Header row: `flex flex-row items-stretch w-full`
- Content area: `flex-1 min-w-0 self-start p-2` — top-aligned with fixed padding
- Right column: `flex flex-col items-end shrink-0` — no padding, no gap
- `bottomRightChildren` wrapper: `flex flex-row` — lays children out horizontally
- `children` wrapper: `w-full` — only rendered when children are provided
### Usage
#### Card with primary and secondary actions
```tsx
import { Card } from "@opal/layouts";
import { Button } from "@opal/components";
import { SvgGlobe, SvgSettings, SvgUnplug, SvgCheckSquare } from "@opal/icons";
<Card.Header
icon={SvgGlobe}
title="Google Search"
description="Web search provider"
sizePreset="main-ui"
variant="section"
rightChildren={
<Button icon={SvgCheckSquare} variant="action" prominence="tertiary">
Current Default
</Button>
}
bottomRightChildren={
<>
<Button icon={SvgUnplug} size="sm" prominence="tertiary" tooltip="Disconnect" />
<Button icon={SvgSettings} size="sm" prominence="tertiary" tooltip="Edit" />
</>
}
/>
```
#### Card with only a connect action
```tsx
<Card.Header
icon={SvgCloud}
title="OpenAI"
description="Not configured"
sizePreset="main-ui"
variant="section"
rightChildren={
<Button rightIcon={SvgArrowExchange} prominence="tertiary">
Connect
</Button>
}
/>
```
#### Card with expandable children
```tsx
<Card.Header
icon={SvgServer}
title="MCP Server"
description="12 tools available"
sizePreset="main-ui"
variant="section"
rightChildren={<Button icon={SvgSettings} prominence="tertiary" />}
>
<SearchBar placeholder="Search tools..." />
</Card.Header>
```
#### No right children
```tsx
<Card.Header
icon={SvgInfo}
title="Section Header"
description="Description text"
sizePreset="main-content"
variant="section"
/>
```
When both `rightChildren` and `bottomRightChildren` are omitted and no `children` are provided, the component renders only the padded `Content`.

View File

@@ -4,16 +4,23 @@ import { Content, type ContentProps } from "@opal/layouts/content/components";
// Types
// ---------------------------------------------------------------------------
type CardHeaderLayoutProps = ContentProps & {
type CardHeaderProps = ContentProps & {
/** Content rendered to the right of the Content block. */
rightChildren?: React.ReactNode;
/** Content rendered below `rightChildren` in the same column. */
bottomRightChildren?: React.ReactNode;
/**
* Content rendered below the header row, full-width.
* Use for expandable sections, search bars, or any content
* that should appear beneath the icon/title/actions row.
*/
children?: React.ReactNode;
};
// ---------------------------------------------------------------------------
// CardHeaderLayout
// Card.Header
// ---------------------------------------------------------------------------
/**
@@ -24,9 +31,12 @@ type CardHeaderLayoutProps = ContentProps & {
* `rightChildren` on top, `bottomRightChildren` below with no
* padding or gap between them.
*
* The optional `children` slot renders below the full header row,
* spanning the entire width.
*
* @example
* ```tsx
* <CardHeaderLayout
* <Card.Header
* icon={SvgGlobe}
* title="Google"
* description="Search engine"
@@ -42,32 +52,42 @@ type CardHeaderLayoutProps = ContentProps & {
* />
* ```
*/
function CardHeaderLayout({
function Header({
rightChildren,
bottomRightChildren,
children,
...contentProps
}: CardHeaderLayoutProps) {
}: CardHeaderProps) {
const hasRight = rightChildren || bottomRightChildren;
return (
<div className="flex flex-row items-stretch w-full">
<div className="flex-1 min-w-0 self-start p-2">
<Content {...contentProps} />
</div>
{hasRight && (
<div className="flex flex-col items-end shrink-0">
{rightChildren && <div className="flex-1">{rightChildren}</div>}
{bottomRightChildren && (
<div className="flex flex-row">{bottomRightChildren}</div>
)}
<div className="flex flex-col w-full">
<div className="flex flex-row items-stretch w-full">
<div className="flex-1 min-w-0 self-start p-2">
<Content {...contentProps} />
</div>
)}
{hasRight && (
<div className="flex flex-col items-end shrink-0">
{rightChildren && <div className="flex-1">{rightChildren}</div>}
{bottomRightChildren && (
<div className="flex flex-row">{bottomRightChildren}</div>
)}
</div>
)}
</div>
{children && <div className="w-full">{children}</div>}
</div>
);
}
// ---------------------------------------------------------------------------
// Card namespace
// ---------------------------------------------------------------------------
const Card = { Header };
// ---------------------------------------------------------------------------
// Exports
// ---------------------------------------------------------------------------
export { CardHeaderLayout, type CardHeaderLayoutProps };
export { Card, type CardHeaderProps };

View File

@@ -1,94 +0,0 @@
# CardHeaderLayout
**Import:** `import { CardHeaderLayout, type CardHeaderLayoutProps } from "@opal/layouts";`
A card header layout that pairs a [`Content`](../../content/README.md) block with a right-side column of vertically stacked children.
## Why CardHeaderLayout?
[`ContentAction`](../../content-action/README.md) provides a single `rightChildren` slot. Card headers typically need two distinct right-side regions — a primary action on top and secondary actions on the bottom. `CardHeaderLayout` provides this with `rightChildren` and `bottomRightChildren` slots, with no padding or gap between them so the caller has full control over spacing.
## Props
Inherits **all** props from [`Content`](../../content/README.md) (icon, title, description, sizePreset, variant, etc.) plus:
| Prop | Type | Default | Description |
|---|---|---|---|
| `rightChildren` | `ReactNode` | `undefined` | Content rendered to the right of the Content block (top of right column). |
| `bottomRightChildren` | `ReactNode` | `undefined` | Content rendered below `rightChildren` in the same column. Laid out as `flex flex-row`. |
## Layout Structure
```
┌──────────────────────────────────────────────────────┐
│ [Content (p-2, self-start)] [rightChildren] │
│ icon + title + description [bottomRightChildren] │
└──────────────────────────────────────────────────────┘
```
- Outer wrapper: `flex flex-row items-stretch w-full`
- Content area: `flex-1 min-w-0 self-start p-2` — top-aligned with fixed padding
- Right column: `flex flex-col items-end justify-between shrink-0` — no padding, no gap
- `bottomRightChildren` wrapper: `flex flex-row` — lays children out horizontally
The right column uses `justify-between` so when both slots are present, `rightChildren` sits at the top and `bottomRightChildren` at the bottom.
## Usage
### Card with primary and secondary actions
```tsx
import { CardHeaderLayout } from "@opal/layouts";
import { Button } from "@opal/components";
import { SvgGlobe, SvgSettings, SvgUnplug, SvgCheckSquare } from "@opal/icons";
<CardHeaderLayout
icon={SvgGlobe}
title="Google Search"
description="Web search provider"
sizePreset="main-ui"
variant="section"
rightChildren={
<Button icon={SvgCheckSquare} variant="action" prominence="tertiary">
Current Default
</Button>
}
bottomRightChildren={
<>
<Button icon={SvgUnplug} size="sm" prominence="tertiary" tooltip="Disconnect" />
<Button icon={SvgSettings} size="sm" prominence="tertiary" tooltip="Edit" />
</>
}
/>
```
### Card with only a connect action
```tsx
<CardHeaderLayout
icon={SvgCloud}
title="OpenAI"
description="Not configured"
sizePreset="main-ui"
variant="section"
rightChildren={
<Button rightIcon={SvgArrowExchange} prominence="tertiary">
Connect
</Button>
}
/>
```
### No right children
```tsx
<CardHeaderLayout
icon={SvgInfo}
title="Section Header"
description="Description text"
sizePreset="main-content"
variant="section"
/>
```
When both `rightChildren` and `bottomRightChildren` are omitted, the component renders only the padded `Content`.

View File

@@ -12,11 +12,8 @@ export {
type ContentActionProps,
} from "@opal/layouts/content-action/components";
/* CardHeaderLayout */
export {
CardHeaderLayout,
type CardHeaderLayoutProps,
} from "@opal/layouts/cards/header-layout/components";
/* Card */
export { Card, type CardHeaderProps } from "@opal/layouts/cards/components";
/* IllustrationContent */
export {

View File

@@ -16,7 +16,7 @@ import Text from "@/refresh-components/texts/Text";
import SidebarWrapper from "@/sections/sidebar/SidebarWrapper";
import SidebarBody from "@/sections/sidebar/SidebarBody";
import SidebarSection from "@/sections/sidebar/SidebarSection";
import UserAvatarPopover from "@/sections/sidebar/UserAvatarPopover";
import AccountPopover from "@/sections/sidebar/AccountPopover";
import Popover, { PopoverMenu } from "@/refresh-components/Popover";
import IconButton from "@/refresh-components/buttons/IconButton";
import ButtonRenaming from "@/refresh-components/buttons/ButtonRenaming";
@@ -398,7 +398,7 @@ const MemoizedBuildSidebarInner = memo(
() => (
<div>
{backToChatButton}
<UserAvatarPopover folded={folded} />
<AccountPopover folded={folded} />
</div>
),
[folded, backToChatButton]

View File

@@ -13,7 +13,7 @@ import {
import { ADMIN_ROUTES } from "@/lib/admin-routes";
import { Section } from "@/layouts/general-layouts";
import { Button, SelectCard } from "@opal/components";
import { CardHeaderLayout } from "@opal/layouts";
import { Card } from "@opal/layouts";
import { Disabled, Hoverable } from "@opal/core";
import Text from "@/refresh-components/texts/Text";
import SimpleLoader from "@/refresh-components/loaders/SimpleLoader";
@@ -113,7 +113,7 @@ export default function CodeInterpreterPage() {
{isEnabled || isLoading ? (
<Hoverable.Root group="code-interpreter/Card">
<SelectCard state="filled" padding="sm" rounding="lg">
<CardHeaderLayout
<Card.Header
sizePreset="main-ui"
variant="section"
icon={SvgTerminal}
@@ -161,7 +161,7 @@ export default function CodeInterpreterPage() {
rounding="lg"
onClick={() => handleToggle(true)}
>
<CardHeaderLayout
<Card.Header
sizePreset="main-ui"
variant="section"
icon={SvgTerminal}

View File

@@ -23,7 +23,7 @@ import Message from "@/refresh-components/messages/Message";
import ConfirmationModalLayout from "@/refresh-components/layouts/ConfirmationModalLayout";
import InputSelect from "@/refresh-components/inputs/InputSelect";
import { Button, SelectCard, Text } from "@opal/components";
import { Content, CardHeaderLayout } from "@opal/layouts";
import { Content, Card } from "@opal/layouts";
import { Hoverable } from "@opal/core";
import {
SvgArrowExchange,
@@ -260,7 +260,7 @@ export default function ImageGenerationContent() {
: undefined
}
>
<CardHeaderLayout
<Card.Header
sizePreset="main-ui"
variant="section"
icon={() => (

View File

@@ -8,7 +8,7 @@ import {
useWellKnownLLMProviders,
} from "@/hooks/useLLMProviders";
import { ThreeDotsLoader } from "@/components/Loading";
import { Content, CardHeaderLayout } from "@opal/layouts";
import { Content, Card } from "@opal/layouts";
import { Button, SelectCard } from "@opal/components";
import { Hoverable } from "@opal/core";
import { SvgArrowExchange, SvgSettings, SvgTrash } from "@opal/icons";
@@ -24,7 +24,7 @@ import { refreshLlmProviderCaches } from "@/lib/llmConfig/cache";
import { deleteLlmProvider, setDefaultLlmModel } from "@/lib/llmConfig/svc";
import Text from "@/refresh-components/texts/Text";
import { Horizontal as HorizontalInput } from "@/layouts/input-layouts";
import Card from "@/refresh-components/cards/Card";
import LegacyCard from "@/refresh-components/cards/Card";
import InputSelect from "@/refresh-components/inputs/InputSelect";
import Message from "@/refresh-components/messages/Message";
import ConfirmationModalLayout from "@/refresh-components/layouts/ConfirmationModalLayout";
@@ -217,7 +217,7 @@ function ExistingProviderCard({
rounding="lg"
onClick={() => setIsOpen(true)}
>
<CardHeaderLayout
<Card.Header
icon={getProviderIcon(provider.provider)}
title={provider.name}
description={getProviderDisplayName(provider.provider)}
@@ -292,7 +292,7 @@ function NewProviderCard({
rounding="lg"
onClick={() => setIsOpen(true)}
>
<CardHeaderLayout
<Card.Header
icon={getProviderIcon(provider.name)}
title={getProviderProductName(provider.name)}
description={getProviderDisplayName(provider.name)}
@@ -336,7 +336,7 @@ function NewCustomProviderCard({
rounding="lg"
onClick={() => setIsOpen(true)}
>
<CardHeaderLayout
<Card.Header
icon={getProviderIcon("custom")}
title={getProviderProductName("custom")}
description={getProviderDisplayName("custom")}
@@ -424,7 +424,7 @@ export default function LLMConfigurationPage() {
<SettingsLayouts.Body>
{hasProviders ? (
<Card>
<LegacyCard>
<HorizontalInput
title="Default Model"
description="This model will be used by Onyx by default in your chats."
@@ -455,7 +455,7 @@ export default function LLMConfigurationPage() {
</InputSelect.Content>
</InputSelect>
</HorizontalInput>
</Card>
</LegacyCard>
) : (
<Message
info

View File

@@ -6,7 +6,7 @@ import { InfoIcon } from "@/components/icons/icons";
import Text from "@/refresh-components/texts/Text";
import { Section } from "@/layouts/general-layouts";
import * as SettingsLayouts from "@/layouts/settings-layouts";
import { Content, CardHeaderLayout } from "@opal/layouts";
import { Content, Card } from "@opal/layouts";
import useSWR from "swr";
import { errorHandlingFetcher, FetchError } from "@/lib/fetcher";
import { SWR_KEYS } from "@/lib/swr-keys";
@@ -275,7 +275,7 @@ function ProviderCard({
: undefined
}
>
<CardHeaderLayout
<Card.Header
sizePreset="main-ui"
variant="section"
icon={icon}

View File

@@ -2,7 +2,7 @@
import type { IconFunctionComponent } from "@opal/types";
import { Button, SelectCard } from "@opal/components";
import { Content, CardHeaderLayout } from "@opal/layouts";
import { Content, Card } from "@opal/layouts";
import {
SvgArrowExchange,
SvgArrowRightCircle,
@@ -15,7 +15,7 @@ import {
* ProviderCard — a stateful card for selecting / connecting / disconnecting
* an external service provider (LLM, search engine, voice model, etc.).
*
* Built on opal `SelectCard` + `CardHeaderLayout`. Maps a three-state
* Built on opal `SelectCard` + `Card.Header`. Maps a three-state
* status model to the `SelectCard` state system:
*
* | Status | SelectCard state | Right action |
@@ -92,7 +92,7 @@ export default function ProviderCard({
aria-label={ariaLabel}
onClick={isDisconnected && onConnect ? onConnect : undefined}
>
<CardHeaderLayout
<Card.Header
sizePreset="main-ui"
variant="section"
icon={icon}

View File

@@ -141,7 +141,7 @@ export interface SettingsProps {
onShowBuildIntro?: () => void;
}
export default function UserAvatarPopover({
export default function AccountPopover({
folded,
onShowBuildIntro,
}: SettingsProps) {

View File

@@ -22,21 +22,17 @@ import { CombinedSettings } from "@/interfaces/settings";
import { SidebarTab } from "@opal/components";
import InputTypeIn from "@/refresh-components/inputs/InputTypeIn";
import Separator from "@/refresh-components/Separator";
import { Disabled } from "@opal/core";
import { SvgArrowUpCircle, SvgSearch, SvgUserManage, SvgX } from "@opal/icons";
import Spacer from "@/refresh-components/Spacer";
import { SvgArrowUpCircle, SvgSearch, SvgX } from "@opal/icons";
import {
useBillingInformation,
useLicense,
hasActiveSubscription,
} from "@/lib/billing";
import { Content } from "@opal/layouts";
import { ADMIN_ROUTES, sidebarItem } from "@/lib/admin-routes";
import useFilter from "@/hooks/useFilter";
import { IconFunctionComponent } from "@opal/types";
import { Section } from "@/layouts/general-layouts";
import Text from "@/refresh-components/texts/Text";
import { getUserDisplayName } from "@/lib/user";
import { APP_SLOGAN } from "@/lib/constants";
import AccountPopover from "@/sections/sidebar/AccountPopover";
const SECTIONS = {
UNLABELED: "",
@@ -267,37 +263,27 @@ function AdminSidebarInner({
return (
<>
<SidebarLayouts.Header>
<div className="flex flex-col w-full">
{folded ? (
<SidebarTab
icon={({ className }) => <SvgX className={className} size={16} />}
href="/app"
variant="sidebar-light"
folded={folded}
icon={SvgSearch}
folded
onClick={() => {
onFoldChange(false);
setFocusSearch(true);
}}
>
Exit Admin Panel
Search
</SidebarTab>
{folded ? (
<SidebarTab
icon={SvgSearch}
folded
onClick={() => {
onFoldChange(false);
setFocusSearch(true);
}}
>
Search
</SidebarTab>
) : (
<InputTypeIn
ref={searchRef}
variant="internal"
leftSearchIcon
placeholder="Search..."
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
)}
</div>
) : (
<InputTypeIn
ref={searchRef}
variant="internal"
leftSearchIcon
placeholder="Search..."
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
)}
</SidebarLayouts.Header>
<SidebarLayouts.Body scrollKey="admin-sidebar">
@@ -343,42 +329,20 @@ function AdminSidebarInner({
<SidebarLayouts.Footer>
{!folded && (
<Section gap={0} height="fit" alignItems="start">
<div className="p-[0.38rem] w-full">
<Content
icon={SvgUserManage}
title={getUserDisplayName(user)}
sizePreset="main-ui"
variant="body"
prominence="muted"
widthVariant="full"
/>
</div>
<div className="flex flex-row gap-1 p-[0.38rem] w-full">
<Text text03 secondaryAction>
<a
className="underline"
href="https://onyx.app"
target="_blank"
>
Onyx
</a>
</Text>
<Text text03 secondaryBody>
|
</Text>
{settings.webVersion ? (
<Text text03 secondaryBody>
{settings.webVersion}
</Text>
) : (
<Text text03 secondaryBody>
{APP_SLOGAN}
</Text>
)}
</div>
</Section>
<>
<Separator noPadding className="px-2" />
<Spacer rem={0.5} />
</>
)}
<SidebarTab
icon={SvgX}
href="/app"
variant="sidebar-light"
folded={folded}
>
Exit Admin Panel
</SidebarTab>
<AccountPopover folded={folded} />
</SidebarLayouts.Footer>
</>
);

View File

@@ -76,7 +76,7 @@ import { track, AnalyticsEvent } from "@/lib/analytics";
import { motion, AnimatePresence } from "motion/react";
import { Notification, NotificationType } from "@/interfaces/settings";
import { errorHandlingFetcher } from "@/lib/fetcher";
import UserAvatarPopover from "@/sections/sidebar/UserAvatarPopover";
import AccountPopover from "@/sections/sidebar/AccountPopover";
import ChatSearchCommandMenu from "@/sections/sidebar/ChatSearchCommandMenu";
import { useQueryController } from "@/providers/QueryControllerProvider";
@@ -593,7 +593,7 @@ const MemoizedAppSidebarInner = memo(function AppSidebarInner() {
{isAdmin ? "Admin Panel" : "Curator Panel"}
</SidebarTab>
)}
<UserAvatarPopover
<AccountPopover
folded={folded}
onShowBuildIntro={
isOnyxCraftEnabled ? handleShowBuildIntro : undefined