diff --git a/cmd/cmd_gateway.go b/cmd/cmd_gateway.go index 3e66c65..65f80b8 100644 --- a/cmd/cmd_gateway.go +++ b/cmd/cmd_gateway.go @@ -3,6 +3,7 @@ package main import ( "context" "fmt" + "io" "net/url" "os" "os/exec" @@ -578,6 +579,12 @@ func applyMaximumPermissionPolicy(cfg *config.Config) { } func gatewayInstallServiceCmd() error { + switch runtime.GOOS { + case "darwin": + return gatewayInstallLaunchdService() + case "windows": + return gatewayInstallWindowsTask() + } scope, unitPath, err := detectGatewayServiceScopeAndPath() if err != nil { return err @@ -615,6 +622,12 @@ func gatewayInstallServiceCmd() error { } func gatewayServiceControlCmd(action string) error { + switch runtime.GOOS { + case "darwin": + return gatewayLaunchdControl(action) + case "windows": + return gatewayWindowsTaskControl(action) + } scope, _, err := detectInstalledGatewayService() if err != nil { return err @@ -631,8 +644,10 @@ func gatewayScopePreference() string { } func detectGatewayServiceScopeAndPath() (string, string, error) { - if runtime.GOOS != "linux" { - return "", "", fmt.Errorf("gateway service registration currently supports Linux systemd only") + switch runtime.GOOS { + case "linux": + default: + return "", "", fmt.Errorf("unsupported service manager for %s", runtime.GOOS) } switch gatewayScopePreference() { case "user": @@ -655,6 +670,12 @@ func userGatewayUnitPath() (string, string, error) { } func detectInstalledGatewayService() (string, string, error) { + switch runtime.GOOS { + case "darwin": + return detectInstalledLaunchdService() + case "windows": + return detectInstalledWindowsTask() + } systemPath := "/etc/systemd/system/" + gatewayServiceName userScope, userPath, err := userGatewayUnitPath() if err != nil { @@ -754,6 +775,271 @@ func runSystemctl(scope string, args ...string) error { return nil } +func gatewayLaunchdLabel() string { return "ai.clawgo.gateway" } + +func gatewayWindowsTaskName() string { return "ClawGo Gateway" } + +func detectLaunchdScopeAndPath() (string, string, error) { + label := gatewayLaunchdLabel() + ".plist" + switch gatewayScopePreference() { + case "system": + return "system", filepath.Join("/Library/LaunchDaemons", label), nil + case "user": + home, err := os.UserHomeDir() + if err != nil { + return "", "", fmt.Errorf("resolve user home failed: %w", err) + } + return "user", filepath.Join(home, "Library", "LaunchAgents", label), nil + } + if os.Geteuid() == 0 { + return "system", filepath.Join("/Library/LaunchDaemons", label), nil + } + home, err := os.UserHomeDir() + if err != nil { + return "", "", fmt.Errorf("resolve user home failed: %w", err) + } + return "user", filepath.Join(home, "Library", "LaunchAgents", label), nil +} + +func detectInstalledLaunchdService() (string, string, error) { + userScope, userPath, err := detectLaunchdScopeAndPath() + if err != nil && gatewayScopePreference() == "user" { + return "", "", err + } + systemPath := filepath.Join("/Library/LaunchDaemons", gatewayLaunchdLabel()+".plist") + systemExists := fileExists(systemPath) + userExists := fileExists(userPath) + + switch gatewayScopePreference() { + case "system": + if systemExists { + return "system", systemPath, nil + } + return "", "", fmt.Errorf("launchd plist not found in system scope: %s", systemPath) + case "user": + if userExists { + return userScope, userPath, nil + } + return "", "", fmt.Errorf("launchd plist not found in user scope: %s", userPath) + } + + if os.Geteuid() == 0 { + if systemExists { + return "system", systemPath, nil + } + if userExists { + return userScope, userPath, nil + } + } else { + if userExists { + return userScope, userPath, nil + } + if systemExists { + return "system", systemPath, nil + } + } + return "", "", fmt.Errorf("gateway service not registered. Run: clawgo gateway") +} + +func gatewayInstallLaunchdService() error { + scope, plistPath, err := detectLaunchdScopeAndPath() + if err != nil { + return err + } + exePath, err := os.Executable() + if err != nil { + return fmt.Errorf("resolve executable path failed: %w", err) + } + exePath, _ = filepath.Abs(exePath) + configPath := getConfigPath() + workDir := filepath.Dir(exePath) + if err := os.MkdirAll(filepath.Dir(plistPath), 0755); err != nil { + return fmt.Errorf("create launchd directory failed: %w", err) + } + content := buildGatewayLaunchdPlist(exePath, configPath, workDir) + if err := os.WriteFile(plistPath, []byte(content), 0644); err != nil { + return fmt.Errorf("write launchd plist failed: %w", err) + } + _ = runLaunchctl(scope, "bootout", launchdDomainTarget(scope), plistPath) + if err := runLaunchctl(scope, "bootstrap", launchdDomainTarget(scope), plistPath); err != nil { + return err + } + if err := runLaunchctl(scope, "kickstart", "-k", launchdServiceTarget(scope)); err != nil { + return err + } + fmt.Printf("✓ Gateway service registered: %s (%s)\n", gatewayLaunchdLabel(), scope) + fmt.Printf(" Launchd plist: %s\n", plistPath) + fmt.Println(" Start service: clawgo gateway start") + fmt.Println(" Restart service: clawgo gateway restart") + fmt.Println(" Stop service: clawgo gateway stop") + return nil +} + +func gatewayLaunchdControl(action string) error { + scope, plistPath, err := detectInstalledLaunchdService() + if err != nil { + return err + } + switch action { + case "start": + _ = runLaunchctl(scope, "bootstrap", launchdDomainTarget(scope), plistPath) + return runLaunchctl(scope, "kickstart", "-k", launchdServiceTarget(scope)) + case "stop": + return runLaunchctl(scope, "bootout", launchdDomainTarget(scope), plistPath) + case "restart": + _ = runLaunchctl(scope, "bootout", launchdDomainTarget(scope), plistPath) + if err := runLaunchctl(scope, "bootstrap", launchdDomainTarget(scope), plistPath); err != nil { + return err + } + return runLaunchctl(scope, "kickstart", "-k", launchdServiceTarget(scope)) + case "status": + return runLaunchctl(scope, "print", launchdServiceTarget(scope)) + default: + return fmt.Errorf("unsupported action: %s", action) + } +} + +func buildGatewayLaunchdPlist(exePath, configPath, workDir string) string { + return fmt.Sprintf(` + + + + Label + %s + ProgramArguments + + %s + gateway + run + --config + %s + + WorkingDirectory + %s + RunAtLoad + + KeepAlive + + StandardOutPath + %s + StandardErrorPath + %s + + +`, gatewayLaunchdLabel(), exePath, configPath, workDir, filepath.Join(filepath.Dir(configPath), "gateway.launchd.out.log"), filepath.Join(filepath.Dir(configPath), "gateway.launchd.err.log")) +} + +func launchdDomainTarget(scope string) string { + if scope == "system" { + return "system" + } + return fmt.Sprintf("gui/%d", os.Getuid()) +} + +func launchdServiceTarget(scope string) string { + return launchdDomainTarget(scope) + "/" + gatewayLaunchdLabel() +} + +func runLaunchctl(scope string, args ...string) error { + cmd := exec.Command("launchctl", args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("launchctl %s failed: %w", strings.Join(args, " "), err) + } + return nil +} + +func gatewayInstallWindowsTask() error { + exePath, err := os.Executable() + if err != nil { + return fmt.Errorf("resolve executable path failed: %w", err) + } + exePath, _ = filepath.Abs(exePath) + configPath := getConfigPath() + taskName := gatewayWindowsTaskName() + command := fmt.Sprintf(`"%s" gateway run --config "%s"`, exePath, configPath) + _ = runSCHTASKS("/Delete", "/TN", taskName, "/F") + if err := runSCHTASKS("/Create", "/TN", taskName, "/SC", "ONLOGON", "/TR", command, "/F"); err != nil { + return err + } + fmt.Printf("✓ Gateway service registered: %s (windows task)\n", taskName) + fmt.Println(" Start service: clawgo gateway start") + fmt.Println(" Restart service: clawgo gateway restart") + fmt.Println(" Stop service: clawgo gateway stop") + return nil +} + +func gatewayWindowsTaskControl(action string) error { + _, _, err := detectInstalledWindowsTask() + if err != nil { + return err + } + taskName := gatewayWindowsTaskName() + switch action { + case "start": + return runSCHTASKS("/Run", "/TN", taskName) + case "stop": + return stopGatewayProcessByPIDFile() + case "restart": + _ = stopGatewayProcessByPIDFile() + return runSCHTASKS("/Run", "/TN", taskName) + case "status": + return runSCHTASKS("/Query", "/TN", taskName, "/V", "/FO", "LIST") + default: + return fmt.Errorf("unsupported action: %s", action) + } +} + +func detectInstalledWindowsTask() (string, string, error) { + taskName := gatewayWindowsTaskName() + if err := runSCHTASKSQuiet("/Query", "/TN", taskName); err != nil { + return "", "", fmt.Errorf("gateway service not registered. Run: clawgo gateway") + } + return "user", taskName, nil +} + +func runSCHTASKS(args ...string) error { + cmd := exec.Command("schtasks", args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("schtasks %s failed: %w", strings.Join(args, " "), err) + } + return nil +} + +func runSCHTASKSQuiet(args ...string) error { + cmd := exec.Command("schtasks", args...) + cmd.Stdout = io.Discard + cmd.Stderr = io.Discard + return cmd.Run() +} + +func stopGatewayProcessByPIDFile() error { + pidPath := filepath.Join(filepath.Dir(getConfigPath()), "gateway.pid") + data, err := os.ReadFile(pidPath) + if err != nil { + return fmt.Errorf("gateway pid file not found: %w", err) + } + pid := strings.TrimSpace(string(data)) + if pid == "" { + return fmt.Errorf("gateway pid file is empty") + } + cmd := exec.Command("taskkill", "/PID", pid, "/T", "/F") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("taskkill /PID %s failed: %w", pid, err) + } + return nil +} + +func fileExists(path string) bool { + info, err := os.Stat(path) + return err == nil && !info.IsDir() +} + func buildGatewayRuntime(ctx context.Context, cfg *config.Config, msgBus *bus.MessageBus, cronService *cron.CronService) (*agent.AgentLoop, *channels.Manager, error) { provider, err := providers.CreateProvider(cfg) if err != nil { diff --git a/cmd/cmd_uninstall.go b/cmd/cmd_uninstall.go index 51c8b75..63195cd 100644 --- a/cmd/cmd_uninstall.go +++ b/cmd/cmd_uninstall.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "runtime" ) func uninstallCmd() { @@ -52,6 +53,28 @@ func uninstallCmd() { } func uninstallGatewayService() error { + switch runtime.GOOS { + case "darwin": + scope, plistPath, err := detectInstalledLaunchdService() + if err != nil { + return nil + } + _ = runLaunchctl(scope, "bootout", launchdDomainTarget(scope), plistPath) + if err := os.Remove(plistPath); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("remove launchd plist failed: %w", err) + } + return nil + case "windows": + _, taskName, err := detectInstalledWindowsTask() + if err != nil { + return nil + } + _ = stopGatewayProcessByPIDFile() + if err := runSCHTASKS("/Delete", "/TN", taskName, "/F"); err != nil { + return err + } + return nil + } scope, unitPath, err := detectInstalledGatewayService() if err != nil { return nil diff --git a/webui/src/components/Button.tsx b/webui/src/components/Button.tsx new file mode 100644 index 0000000..ee6dda8 --- /dev/null +++ b/webui/src/components/Button.tsx @@ -0,0 +1,208 @@ +import React from 'react'; + +type ButtonVariant = 'neutral' | 'primary' | 'accent' | 'success' | 'warning' | 'danger'; +type FixedButtonShape = 'icon' | 'square'; +type ButtonSize = 'sm' | 'md' | 'xs' | 'xs_tall' | 'md_tall' | 'md_wide'; +type ButtonRadius = 'default' | 'lg' | 'xl' | 'full'; +type ButtonGap = 'none' | '1' | '2'; +type NativeButtonProps = Omit, 'className'>; +type NativeAnchorProps = Omit, 'className'>; + +type ButtonStyleProps = { + radius?: ButtonRadius; + gap?: ButtonGap; + shadow?: boolean; + noShrink?: boolean; +}; + +type ButtonProps = NativeButtonProps & ButtonStyleProps & { + variant?: ButtonVariant; + size?: ButtonSize; + fullWidth?: boolean; + grow?: boolean; +}; + +type LinkButtonProps = NativeAnchorProps & ButtonStyleProps & { + variant?: ButtonVariant; + size?: ButtonSize; + fullWidth?: boolean; + grow?: boolean; +}; + +type FixedButtonProps = NativeButtonProps & ButtonStyleProps & { + label: string; + shape?: FixedButtonShape; + variant?: ButtonVariant; +}; + +type FixedLinkButtonProps = NativeAnchorProps & ButtonStyleProps & { + label: string; + shape?: FixedButtonShape; + variant?: ButtonVariant; +}; + +function joinClasses(...values: Array) { + return values.filter(Boolean).join(' '); +} + +function buttonSizeClass(size: ButtonSize) { + switch (size) { + case 'xs': + return 'px-2.5 py-1 text-xs'; + case 'xs_tall': + return 'px-2.5 py-2 text-xs'; + case 'sm': + return 'px-3 py-1.5 text-sm'; + case 'md_tall': + return 'px-4 py-2.5 text-sm font-medium'; + case 'md_wide': + return 'px-6 py-2 text-sm font-medium'; + case 'md': + default: + return 'px-4 py-2 text-sm font-medium'; + } +} + +function buttonRadiusClass(radius: ButtonRadius) { + switch (radius) { + case 'lg': + return 'rounded-lg'; + case 'xl': + return 'rounded-xl'; + case 'full': + return 'rounded-full'; + case 'default': + default: + return undefined; + } +} + +function buttonGapClass(gap: ButtonGap) { + switch (gap) { + case '1': + return 'gap-1'; + case '2': + return 'gap-2'; + case 'none': + default: + return undefined; + } +} + +function buttonClass( + variant: ButtonVariant, + size: ButtonSize, + fullWidth: boolean, + grow: boolean, + radius: ButtonRadius, + gap: ButtonGap, + shadow: boolean, + noShrink: boolean, +) { + return joinClasses( + 'ui-button', + `ui-button-${variant}`, + buttonSizeClass(size), + fullWidth && 'w-full', + grow && 'flex-1', + buttonRadiusClass(radius), + buttonGapClass(gap), + shadow && 'shadow-sm', + noShrink && 'shrink-0', + ); +} + +export function Button({ + variant = 'neutral', + size = 'md', + fullWidth = false, + grow = false, + radius = 'default', + gap = 'none', + shadow = false, + noShrink = false, + type = 'button', + ...props +}: ButtonProps) { + return + ); +} + +export function FixedLinkButton({ + label, + shape = 'icon', + variant = 'neutral', + radius = 'default', + gap = 'none', + shadow = false, + noShrink = false, + title, + children, + ...props +}: FixedLinkButtonProps) { + return ( + + {children} + + ); +} diff --git a/webui/src/components/GlobalDialog.tsx b/webui/src/components/GlobalDialog.tsx index db084ec..e88a44d 100644 --- a/webui/src/components/GlobalDialog.tsx +++ b/webui/src/components/GlobalDialog.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { AnimatePresence, motion } from 'motion/react'; import { useTranslation } from 'react-i18next'; +import { Button } from './Button'; type DialogOptions = { title?: string; @@ -61,11 +62,11 @@ export const GlobalDialog: React.FC<{
{(kind === 'confirm' || kind === 'prompt') && ( - + )} - +
diff --git a/webui/src/components/Header.tsx b/webui/src/components/Header.tsx index a065a10..98c19d4 100644 --- a/webui/src/components/Header.tsx +++ b/webui/src/components/Header.tsx @@ -3,6 +3,7 @@ import { Github, Moon, RefreshCw, SunMedium, Terminal } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { useAppContext } from '../context/AppContext'; import { useUI } from '../context/UIContext'; +import { FixedButton, FixedLinkButton } from './Button'; const REPO_URL = 'https://github.com/YspCoder/clawgo'; @@ -88,41 +89,21 @@ const Header: React.FC = () => {
- + - + - + - + - +
); diff --git a/webui/src/components/RecursiveConfig.tsx b/webui/src/components/RecursiveConfig.tsx index 8ccc4aa..b2f8242 100644 --- a/webui/src/components/RecursiveConfig.tsx +++ b/webui/src/components/RecursiveConfig.tsx @@ -1,6 +1,7 @@ import React, { useMemo, useState } from 'react'; import { Plus } from 'lucide-react'; import { useTranslation } from 'react-i18next'; +import { FixedButton } from './Button'; interface RecursiveConfigProps { data: any; @@ -79,17 +80,16 @@ const PrimitiveArrayEditor: React.FC<{ ))} - + setSessionKey(e.target.value)} className="ui-select min-w-[220px] flex-1 rounded-xl px-2.5 py-1.5 text-xs"> {userSessions.map((s: any) => )} )} - + {chatTab === 'subagents' && (
- + {streamActors.map((agent) => ( - + ))}
)} @@ -654,13 +641,9 @@ const Chat: React.FC = () => { placeholder={t('subagentLabelPlaceholder')} className="ui-input w-full rounded-2xl px-3 py-2.5 text-sm" /> - +
{t('agents')}
diff --git a/webui/src/pages/Config.tsx b/webui/src/pages/Config.tsx index b02852a..e0bc979 100644 --- a/webui/src/pages/Config.tsx +++ b/webui/src/pages/Config.tsx @@ -3,6 +3,7 @@ import { Plus, RefreshCw, Save } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { useAppContext } from '../context/AppContext'; import { useUI } from '../context/UIContext'; +import { Button, FixedButton } from '../components/Button'; import RecursiveConfig from '../components/RecursiveConfig'; function setPath(obj: any, path: string, value: any) { @@ -283,29 +284,27 @@ const Config: React.FC = () => {

{t('configuration')}

- - + +
- +
- - - + +
@@ -369,7 +363,7 @@ const Config: React.FC = () => { updateProxyField(name, 'api_base', e.target.value)} placeholder={t('configLabels.api_base')} className="md:col-span-2 px-2 py-1 rounded-lg bg-zinc-950/70 border border-zinc-800" /> updateProxyField(name, 'api_key', e.target.value)} placeholder={t('configLabels.api_key')} className="md:col-span-2 px-2 py-1 rounded-lg bg-zinc-950/70 border border-zinc-800" /> updateProxyField(name, 'models', e.target.value.split(',').map(s=>s.trim()).filter(Boolean))} placeholder={`${t('configLabels.models')}${t('configCommaSeparatedHint')}`} className="md:col-span-1 px-2 py-1 rounded-lg bg-zinc-950/70 border border-zinc-800" /> - +
))} {Object.keys(((cfg as any)?.providers?.proxies || {}) as Record).length === 0 && ( @@ -417,14 +411,9 @@ const Config: React.FC = () => {
{t('configNodeP2PIceServers')}
- +
{Array.isArray((cfg as any)?.gateway?.nodes?.p2p?.ice_servers) && (cfg as any).gateway.nodes.p2p.ice_servers.length > 0 ? ( ((cfg as any).gateway.nodes.p2p.ice_servers as Array).map((server, index) => ( @@ -447,7 +436,7 @@ const Config: React.FC = () => { placeholder={t('configNodeP2PIceCredential')} className="md:col-span-2 px-2 py-1 rounded-lg bg-zinc-950/70 border border-zinc-800" /> - +
)) ) : ( @@ -623,7 +612,7 @@ const Config: React.FC = () => {
{t('configDiffPreviewCount', { count: diffRows.length })}
- +
diff --git a/webui/src/pages/Cron.tsx b/webui/src/pages/Cron.tsx index 47ad8d6..2c5b873 100644 --- a/webui/src/pages/Cron.tsx +++ b/webui/src/pages/Cron.tsx @@ -4,6 +4,7 @@ import { motion, AnimatePresence } from 'motion/react'; import { useTranslation } from 'react-i18next'; import { useAppContext } from '../context/AppContext'; import { useUI } from '../context/UIContext'; +import { Button, FixedButton } from '../components/Button'; import { CronJob } from '../types'; import { formatLocalDateTime } from '../utils/time'; @@ -172,22 +173,12 @@ const Cron: React.FC = () => {

{t('cronJobs')}

- - +
@@ -223,26 +214,20 @@ const Cron: React.FC = () => {
- - - +
); @@ -273,9 +258,9 @@ const Cron: React.FC = () => { >

{editingCron ? t('editJob') : t('addJob')}

- +
@@ -376,18 +361,10 @@ const Cron: React.FC = () => {
- - + +
diff --git a/webui/src/pages/Dashboard.tsx b/webui/src/pages/Dashboard.tsx index ecf2746..f327f82 100644 --- a/webui/src/pages/Dashboard.tsx +++ b/webui/src/pages/Dashboard.tsx @@ -3,6 +3,7 @@ import { RefreshCw, Activity, MessageSquare, Wrench, Sparkles, AlertTriangle, Wo import { useTranslation } from 'react-i18next'; import { useAppContext } from '../context/AppContext'; import StatCard from '../components/StatCard'; +import { FixedButton } from '../components/Button'; function formatRuntimeTime(value: unknown) { const raw = String(value || '').trim(); @@ -122,14 +123,9 @@ const Dashboard: React.FC = () => { {t('webui')}: {webuiVersion} - +
diff --git a/webui/src/pages/EKG.tsx b/webui/src/pages/EKG.tsx index caf7485..0590116 100644 --- a/webui/src/pages/EKG.tsx +++ b/webui/src/pages/EKG.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useMemo, useState } from 'react'; import { AlertTriangle, RefreshCw, Route, ServerCrash, Workflow } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { useAppContext } from '../context/AppContext'; +import { FixedButton } from '../components/Button'; type EKGKV = { key?: string; score?: number; count?: number }; @@ -164,14 +165,14 @@ const EKG: React.FC = () => { - +
diff --git a/webui/src/pages/Logs.tsx b/webui/src/pages/Logs.tsx index b7e832b..01712c9 100644 --- a/webui/src/pages/Logs.tsx +++ b/webui/src/pages/Logs.tsx @@ -5,6 +5,7 @@ import { useAppContext } from '../context/AppContext'; import { useUI } from '../context/UIContext'; import { LogEntry } from '../types'; import { formatLocalTime } from '../utils/time'; +import { Button } from '../components/Button'; const Logs: React.FC = () => { const { t } = useTranslation(); @@ -174,23 +175,15 @@ const Logs: React.FC = () => {
- - + - + +
diff --git a/webui/src/pages/MCP.tsx b/webui/src/pages/MCP.tsx index 8fb134c..d0a0202 100644 --- a/webui/src/pages/MCP.tsx +++ b/webui/src/pages/MCP.tsx @@ -4,6 +4,7 @@ import { Package, Pencil, Plus, RefreshCw, Save, Trash2, Wrench, X } from 'lucid import { useTranslation } from 'react-i18next'; import { useAppContext } from '../context/AppContext'; import { useUI } from '../context/UIContext'; +import { Button, FixedButton } from '../components/Button'; type MCPDraftServer = { enabled: boolean; @@ -356,22 +357,12 @@ const MCP: React.FC = () => {

{t('mcpServicesHint')}

- - +
@@ -405,12 +396,12 @@ const MCP: React.FC = () => { )}
- - +
@@ -476,9 +467,9 @@ const MCP: React.FC = () => {
{t('mcpServicesHint')}
- +
@@ -551,9 +542,9 @@ const MCP: React.FC = () => {
Args
{t('configMCPArgsEnterHint')}
- +
@@ -587,9 +578,9 @@ const MCP: React.FC = () => {
{t('configMCPInstallSuggested', { pkg: activeCheck.package })}
)} {activeCheck.installable && ( - + )}
)} @@ -601,14 +592,14 @@ const MCP: React.FC = () => {
{editingName && ( - + )} - - + +
diff --git a/webui/src/pages/Memory.tsx b/webui/src/pages/Memory.tsx index b5539c1..17f498c 100644 --- a/webui/src/pages/Memory.tsx +++ b/webui/src/pages/Memory.tsx @@ -3,6 +3,7 @@ import { Trash2 } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { useAppContext } from '../context/AppContext'; import { useUI } from '../context/UIContext'; +import { Button, FixedButton } from '../components/Button'; const Memory: React.FC = () => { const { t } = useTranslation(); @@ -118,7 +119,9 @@ const Memory: React.FC = () => {

{t('memoryFiles')}

- + + + +
{files.map((f) => ( @@ -141,7 +144,7 @@ const Memory: React.FC = () => {

{active || t('noFileSelected')}

- +