fix(terminal): resolve PTY shell exec error, simplify CLI, unify Config tabs, restore Studio CSS
- Fix detectShell() to return full paths via LookPath (was returning bare names causing exec error on some systems) - Add shell path validation before pty.Start to prevent crashes - Simplify CLI: remove all subcommands, keep only desktop launch with --port - Restore missing Studio shared CSS (code blocks, input area, animations) - Replace Config vertical sidebar with horizontal nav-tabs matching main layout 💘 Generated with Crush Assisted-by: GLM-5.1 via Crush <crush@charm.land>
This commit is contained in:
@@ -3,103 +3,15 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/muyue/muyue/internal/config"
|
||||
"github.com/muyue/muyue/internal/desktop"
|
||||
"github.com/muyue/muyue/internal/installer"
|
||||
"github.com/muyue/muyue/internal/lsp"
|
||||
"github.com/muyue/muyue/internal/mcp"
|
||||
"github.com/muyue/muyue/internal/orchestrator"
|
||||
"github.com/muyue/muyue/internal/profiler"
|
||||
"github.com/muyue/muyue/internal/scanner"
|
||||
"github.com/muyue/muyue/internal/skills"
|
||||
"github.com/muyue/muyue/internal/updater"
|
||||
"github.com/muyue/muyue/internal/version"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) > 1 {
|
||||
if isCommand(os.Args[1]) {
|
||||
handleCommand(os.Args[1:])
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
runDesktop(os.Args[1:])
|
||||
}
|
||||
|
||||
func isCommand(arg string) bool {
|
||||
switch arg {
|
||||
case "version", "-v", "--version",
|
||||
"scan", "install", "update", "setup",
|
||||
"config", "doctor", "lsp", "mcp", "skills",
|
||||
"help", "-h", "--help":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func handleCommand(args []string) {
|
||||
switch args[0] {
|
||||
case "version", "-v", "--version":
|
||||
fmt.Println(version.FullVersion())
|
||||
case "scan":
|
||||
runScan()
|
||||
case "install":
|
||||
runInstall(args[1:])
|
||||
case "update":
|
||||
runUpdate()
|
||||
case "setup":
|
||||
runSetup()
|
||||
case "config":
|
||||
showConfig()
|
||||
case "doctor":
|
||||
runDoctor()
|
||||
case "lsp":
|
||||
runLSP(args[1:])
|
||||
case "mcp":
|
||||
runMCP(args[1:])
|
||||
case "skills":
|
||||
runSkills(args[1:])
|
||||
case "help", "-h", "--help":
|
||||
printHelp()
|
||||
}
|
||||
}
|
||||
|
||||
func printHelp() {
|
||||
fmt.Printf(`%s - AI-powered development environment assistant
|
||||
|
||||
Usage:
|
||||
muyue Launch desktop app (opens browser)
|
||||
muyue <command> Run a specific command
|
||||
|
||||
Options:
|
||||
--port=PORT Specify port (default: auto)
|
||||
--no-open Don't open browser automatically
|
||||
|
||||
Commands:
|
||||
version Show version
|
||||
scan Scan your system for tools and runtimes
|
||||
install [tools] Install missing tools (needs sudo for some tools)
|
||||
update Check and apply updates for all tools
|
||||
setup Run first-time setup wizard
|
||||
config Show current configuration
|
||||
doctor Check that everything is properly configured
|
||||
lsp [scan|install] Scan or install LSP servers
|
||||
mcp [config|scan] Configure MCP servers for Crush and Claude Code
|
||||
skills [list|generate|deploy|init|delete] Manage AI coding skills
|
||||
help Show this help
|
||||
|
||||
Note:
|
||||
Some tools (docker, gh, etc.) require elevated privileges.
|
||||
Run 'sudo muyue install' or use 'pkexec muyue install' if needed.
|
||||
`, version.FullVersion())
|
||||
}
|
||||
|
||||
func runDesktop(args []string) {
|
||||
cfg := loadOrSetupConfig()
|
||||
if err := desktop.Run(cfg, args); err != nil {
|
||||
if err := desktop.Run(cfg, os.Args[1:]); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -140,453 +52,3 @@ func loadOrSetupConfig() *config.MuyueConfig {
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
func runScan() {
|
||||
fmt.Println("Scanning system...")
|
||||
result := scanner.ScanSystem()
|
||||
fmt.Println(result.Summary())
|
||||
}
|
||||
|
||||
func runInstall(tools []string) {
|
||||
cfg := loadOrSetupConfig()
|
||||
inst := installer.New(cfg)
|
||||
|
||||
if len(tools) == 0 {
|
||||
result := scanner.ScanSystem()
|
||||
var missing []string
|
||||
for _, t := range result.Tools {
|
||||
if !t.Installed {
|
||||
missing = append(missing, t.Name)
|
||||
}
|
||||
}
|
||||
|
||||
if len(missing) == 0 {
|
||||
fmt.Println("All tools are installed!")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Missing tools: %v\nInstalling...\n", missing)
|
||||
tools = missing
|
||||
}
|
||||
|
||||
if needsSudo(tools) && os.Geteuid() != 0 {
|
||||
fmt.Println("Some tools require elevated privileges.")
|
||||
if path, err := exec.LookPath("sudo"); err == nil {
|
||||
fmt.Printf("Re-running with sudo...\n")
|
||||
cmd := exec.Command(path, append([]string{os.Args[0], "install"}, tools...)...)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "sudo install failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
config.Save(cfg)
|
||||
return
|
||||
}
|
||||
if path, err := exec.LookPath("pkexec"); err == nil {
|
||||
fmt.Printf("Re-running with pkexec...\n")
|
||||
cmd := exec.Command(path, append([]string{os.Args[0], "install"}, tools...)...)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "pkexec install failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
config.Save(cfg)
|
||||
return
|
||||
}
|
||||
fmt.Println("Neither sudo nor pkexec found. Some installs may fail.")
|
||||
fmt.Println("Try running: sudo muyue install")
|
||||
}
|
||||
|
||||
results := inst.InstallAll(tools)
|
||||
for _, r := range results {
|
||||
status := "[OK]"
|
||||
if !r.Success {
|
||||
status = "[FAIL]"
|
||||
}
|
||||
fmt.Printf(" %s %s: %s\n", status, r.Tool, r.Message)
|
||||
}
|
||||
|
||||
config.Save(cfg)
|
||||
}
|
||||
|
||||
func needsSudo(tools []string) bool {
|
||||
sudoTools := map[string]bool{
|
||||
"docker": true, "git": true, "gh": true, "node": true, "python": true,
|
||||
}
|
||||
for _, t := range tools {
|
||||
if sudoTools[t] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func runUpdate() {
|
||||
fmt.Println("Checking for updates...")
|
||||
result := scanner.ScanSystem()
|
||||
statuses := updater.CheckUpdates(result)
|
||||
|
||||
needsUpdate := false
|
||||
for _, s := range statuses {
|
||||
if s.NeedsUpdate {
|
||||
fmt.Printf(" [!] %s: %s -> %s\n", s.Tool, s.Current, s.Latest)
|
||||
needsUpdate = true
|
||||
} else if s.Error == "" {
|
||||
fmt.Printf(" [v] %s: up to date (%s)\n", s.Tool, s.Current)
|
||||
} else {
|
||||
fmt.Printf(" [?] %s: %s\n", s.Tool, s.Error)
|
||||
}
|
||||
}
|
||||
|
||||
if needsUpdate {
|
||||
fmt.Println("\nApplying updates...")
|
||||
results := updater.RunAutoUpdate(statuses)
|
||||
for _, r := range results {
|
||||
fmt.Printf(" %s: %s\n", r.Tool, r.Message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func runSetup() {
|
||||
cfg, err := profiler.RunFirstTimeSetup()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Setup error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
for i := range cfg.AI.Providers {
|
||||
if cfg.AI.Providers[i].Active && cfg.AI.Providers[i].APIKey == "" {
|
||||
key, err := profiler.AskAPIKey(cfg.AI.Providers[i].Name)
|
||||
if err == nil && key != "" {
|
||||
cfg.AI.Providers[i].APIKey = key
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := config.Save(cfg); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Save error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Println("Setup complete!")
|
||||
}
|
||||
|
||||
func showConfig() {
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Config not found. Run `muyue setup` first.\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("Profile: %s (%s)\n", cfg.Profile.Name, cfg.Profile.Pseudo)
|
||||
fmt.Printf("Email: %s\n", cfg.Profile.Email)
|
||||
fmt.Printf("Editor: %s\n", cfg.Profile.Preferences.Editor)
|
||||
fmt.Printf("Default AI: %s\n", cfg.Profile.Preferences.DefaultAI)
|
||||
fmt.Printf("Languages: %v\n", cfg.Profile.Languages)
|
||||
|
||||
for _, p := range cfg.AI.Providers {
|
||||
active := ""
|
||||
if p.Active {
|
||||
active = " (active)"
|
||||
}
|
||||
keyStatus := "no key"
|
||||
if p.APIKey != "" {
|
||||
keyStatus = "configured"
|
||||
}
|
||||
fmt.Printf(" %s: model=%s, key=%s%s\n", p.Name, p.Model, keyStatus, active)
|
||||
}
|
||||
|
||||
fmt.Printf("BMAD: installed=%v, global=%v\n", cfg.BMAD.Installed, cfg.BMAD.Global)
|
||||
fmt.Printf("Custom Prompt: %v\n", cfg.Terminal.CustomPrompt)
|
||||
}
|
||||
|
||||
func runDoctor() {
|
||||
ok := true
|
||||
fmt.Println("Running diagnostics...")
|
||||
fmt.Println()
|
||||
|
||||
fmt.Println("Configuration:")
|
||||
if !config.Exists() {
|
||||
fmt.Println(" [FAIL] Config file not found. Run 'muyue setup' first.")
|
||||
ok = false
|
||||
} else {
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
fmt.Printf(" [FAIL] Config load error: %v\n", err)
|
||||
ok = false
|
||||
} else {
|
||||
fmt.Println(" [OK] Config file present")
|
||||
hasKey := false
|
||||
for _, p := range cfg.AI.Providers {
|
||||
if p.Active && p.APIKey != "" {
|
||||
hasKey = true
|
||||
}
|
||||
}
|
||||
if hasKey {
|
||||
fmt.Println(" [OK] API key configured")
|
||||
} else {
|
||||
fmt.Println(" [FAIL] No API key set for active provider")
|
||||
ok = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("\nTools:")
|
||||
result := scanner.ScanSystem()
|
||||
installed := 0
|
||||
for _, t := range result.Tools {
|
||||
if t.Installed {
|
||||
installed++
|
||||
fmt.Printf(" [OK] %s\n", t.Name)
|
||||
} else {
|
||||
fmt.Printf(" [FAIL] %s (not installed)\n", t.Name)
|
||||
}
|
||||
}
|
||||
fmt.Printf(" Installed: %d/%d\n", installed, len(result.Tools))
|
||||
|
||||
fmt.Println("\nLSP Servers:")
|
||||
servers := lsp.ScanServers()
|
||||
lspOK := 0
|
||||
for _, s := range servers {
|
||||
if s.Installed {
|
||||
lspOK++
|
||||
fmt.Printf(" [OK] %s (%s)\n", s.Name, s.Language)
|
||||
}
|
||||
}
|
||||
fmt.Printf(" Available: %d/%d\n", lspOK, len(servers))
|
||||
|
||||
fmt.Println("\nMCP Servers:")
|
||||
mcpServers := mcp.ScanServers()
|
||||
mcpOK := 0
|
||||
for _, s := range mcpServers {
|
||||
if s.Installed {
|
||||
mcpOK++
|
||||
}
|
||||
}
|
||||
fmt.Printf(" Available: %d/%d\n", mcpOK, len(mcpServers))
|
||||
|
||||
fmt.Println("\nSkills:")
|
||||
skillList, err := skills.List()
|
||||
if err != nil || len(skillList) == 0 {
|
||||
fmt.Println(" [FAIL] No skills. Run 'muyue skills init'.")
|
||||
ok = false
|
||||
} else {
|
||||
fmt.Printf(" [OK] %d skills installed\n", len(skillList))
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
if ok {
|
||||
fmt.Println("All checks passed!")
|
||||
} else {
|
||||
fmt.Println("Some checks failed. Review the output above.")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func runLSP(args []string) {
|
||||
if len(args) == 0 {
|
||||
args = []string{"scan"}
|
||||
}
|
||||
|
||||
switch args[0] {
|
||||
case "scan":
|
||||
fmt.Println("Scanning LSP servers...")
|
||||
servers := lsp.ScanServers()
|
||||
installed := 0
|
||||
for _, s := range servers {
|
||||
if s.Installed {
|
||||
installed++
|
||||
fmt.Printf(" [v] %-35s (%s)\n", s.Name, s.Language)
|
||||
} else {
|
||||
fmt.Printf(" [ ] %-35s (%s)\n", s.Name, s.Language)
|
||||
}
|
||||
}
|
||||
fmt.Printf("\nInstalled: %d/%d\n", installed, len(servers))
|
||||
case "install":
|
||||
if len(args) < 2 {
|
||||
cfg := loadOrSetupConfig()
|
||||
fmt.Printf("Installing LSP servers for: %v\n", cfg.Profile.Languages)
|
||||
results := lsp.InstallForLanguages(cfg.Profile.Languages)
|
||||
for _, r := range results {
|
||||
if r.Installed {
|
||||
fmt.Printf(" [OK] %s (%s)\n", r.Name, r.Language)
|
||||
} else {
|
||||
fmt.Printf(" [FAIL] %s (%s)\n", r.Name, r.Language)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, name := range args[1:] {
|
||||
fmt.Printf("Installing %s...\n", name)
|
||||
if err := lsp.InstallServer(name); err != nil {
|
||||
fmt.Printf(" [FAIL] %s: %s\n", name, err)
|
||||
} else {
|
||||
fmt.Printf(" [OK] %s\n", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
fmt.Printf("Unknown lsp subcommand: %s (scan, install)\n", args[0])
|
||||
}
|
||||
}
|
||||
|
||||
func runMCP(args []string) {
|
||||
if len(args) == 0 {
|
||||
args = []string{"config"}
|
||||
}
|
||||
|
||||
switch args[0] {
|
||||
case "config":
|
||||
cfg := loadOrSetupConfig()
|
||||
fmt.Println("Configuring MCP servers for Crush and Claude Code...")
|
||||
if err := mcp.ConfigureAll(cfg); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("Done! MCP servers configured.")
|
||||
case "scan":
|
||||
fmt.Println("Scanning MCP servers...")
|
||||
servers := mcp.ScanServers()
|
||||
available := 0
|
||||
for _, s := range servers {
|
||||
if s.Installed {
|
||||
available++
|
||||
fmt.Printf(" [v] %-30s (%s)\n", s.Name, s.Category)
|
||||
} else {
|
||||
fmt.Printf(" [ ] %-30s (%s)\n", s.Name, s.Category)
|
||||
}
|
||||
}
|
||||
fmt.Printf("\nAvailable: %d/%d\n", available, len(servers))
|
||||
default:
|
||||
fmt.Printf("Unknown mcp subcommand: %s (config, scan)\n", args[0])
|
||||
}
|
||||
}
|
||||
|
||||
func runSkills(args []string) {
|
||||
if len(args) == 0 {
|
||||
args = []string{"list"}
|
||||
}
|
||||
|
||||
switch args[0] {
|
||||
case "list", "ls":
|
||||
skillsList, err := skills.List()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if len(skillsList) == 0 {
|
||||
fmt.Println("No skills found. Run `muyue skills init` to install built-in skills.")
|
||||
return
|
||||
}
|
||||
fmt.Printf("Skills (%d):\n", len(skillsList))
|
||||
for _, s := range skillsList {
|
||||
target := s.Target
|
||||
if target == "" {
|
||||
target = "both"
|
||||
}
|
||||
fmt.Printf(" %-20s %-8s %s\n", s.Name, target, s.Description)
|
||||
}
|
||||
|
||||
case "init":
|
||||
fmt.Println("Installing built-in skills...")
|
||||
if err := skills.InstallBuiltinSkills(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("Deploying to Crush and Claude Code...")
|
||||
if err := skills.DeployAll(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Deploy error: %v\n", err)
|
||||
}
|
||||
fmt.Println("Done! Built-in skills installed and deployed.")
|
||||
|
||||
case "show":
|
||||
if len(args) < 2 {
|
||||
fmt.Println("Usage: muyue skills show <name>")
|
||||
return
|
||||
}
|
||||
skill, err := skills.Get(args[1])
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("Name: %s\n", skill.Name)
|
||||
fmt.Printf("Description: %s\n", skill.Description)
|
||||
fmt.Printf("Author: %s\n", skill.Author)
|
||||
fmt.Printf("Version: %s\n", skill.Version)
|
||||
fmt.Printf("Target: %s\n", skill.Target)
|
||||
fmt.Printf("Tags: %v\n", skill.Tags)
|
||||
fmt.Printf("Path: %s\n", skill.FilePath)
|
||||
fmt.Printf("\n--- Content ---\n%s\n", skill.Content)
|
||||
|
||||
case "generate":
|
||||
if len(args) < 3 {
|
||||
fmt.Println("Usage: muyue skills generate <name> <description> [crush|claude|both]")
|
||||
fmt.Println("Example: muyue skills generate docker-setup \"Set up Docker for a project\" both")
|
||||
return
|
||||
}
|
||||
name := args[1]
|
||||
description := args[2]
|
||||
target := "both"
|
||||
if len(args) > 3 {
|
||||
target = args[3]
|
||||
}
|
||||
|
||||
cfg := loadOrSetupConfig()
|
||||
orch, err := orchestrator.New(cfg)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "AI not configured: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("Generating skill '%s'...\n", name)
|
||||
prompt := skills.BuildAIGeneratePrompt(name, description, target)
|
||||
resp, err := orch.Send(prompt)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Generation error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
skill := &skills.Skill{
|
||||
Name: name,
|
||||
Description: description,
|
||||
Content: resp,
|
||||
Author: "muyue-generated",
|
||||
Version: "0.1.0",
|
||||
Target: target,
|
||||
Tags: []string{"generated"},
|
||||
}
|
||||
|
||||
if err := skills.Create(skill); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Save error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("Skill '%s' created and deployed!\n", name)
|
||||
|
||||
case "deploy":
|
||||
fmt.Println("Deploying all skills to Crush and Claude Code...")
|
||||
if err := skills.DeployAll(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("Done!")
|
||||
|
||||
case "delete":
|
||||
if len(args) < 2 {
|
||||
fmt.Println("Usage: muyue skills delete <name>")
|
||||
return
|
||||
}
|
||||
if err := skills.Delete(args[1]); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("Skill '%s' deleted.\n", args[1])
|
||||
|
||||
default:
|
||||
fmt.Printf("Unknown skills subcommand: %s\n", args[0])
|
||||
fmt.Println("Available: list, show, generate, deploy, init, delete")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,10 +82,19 @@ func (s *Server) handleTerminalWS(w http.ResponseWriter, r *http.Request) {
|
||||
shell := initMsg.Data
|
||||
if shell == "" {
|
||||
shell = detectShell()
|
||||
} else {
|
||||
if path, err := exec.LookPath(shell); err == nil {
|
||||
shell = path
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := os.Stat(shell); err != nil {
|
||||
conn.WriteJSON(wsMessage{Type: "error", Data: fmt.Sprintf("shell not found: %s", shell)})
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Contains(shell, "wsl") {
|
||||
cmd = exec.Command("wsl", "--shell-type", "login")
|
||||
cmd = exec.Command(shell, "--shell-type", "login")
|
||||
} else if strings.Contains(shell, "powershell") || strings.Contains(shell, "pwsh") {
|
||||
cmd = exec.Command(shell, "-NoLogo", "-NoProfile")
|
||||
} else {
|
||||
@@ -250,8 +259,8 @@ func (s *Server) handleTerminalSessionsDelete(w http.ResponseWriter, r *http.Req
|
||||
func detectShell() string {
|
||||
shells := []string{"zsh", "bash", "fish", "pwsh", "powershell"}
|
||||
for _, s := range shells {
|
||||
if _, err := exec.LookPath(s); err == nil {
|
||||
return s
|
||||
if path, err := exec.LookPath(s); err == nil {
|
||||
return path
|
||||
}
|
||||
}
|
||||
return "/bin/sh"
|
||||
|
||||
@@ -132,27 +132,23 @@ export default function Config({ api }) {
|
||||
<div className="config-window">
|
||||
{toast && <div className="config-toast">{toast}</div>}
|
||||
|
||||
<div className="config-sidebar">
|
||||
<div className="config-tabs-bar">
|
||||
{PANELS.map(p => {
|
||||
const Icon = p.icon
|
||||
return (
|
||||
<div
|
||||
key={p.id}
|
||||
className={`config-sidebar-item ${activePanel === p.id ? 'active' : ''}`}
|
||||
className={`nav-tab ${activePanel === p.id ? 'active' : ''}`}
|
||||
onClick={() => setActivePanel(p.id)}
|
||||
>
|
||||
<Icon size={16} />
|
||||
<span>{t(`config.panels.${p.id}`)}</span>
|
||||
<span className="tab-icon"><Icon size={15} /></span>
|
||||
{t(`config.panels.${p.id}`)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="config-panel-area">
|
||||
<div className="config-panel-header">
|
||||
<h2 className="config-panel-title">{t(`config.panels.${activePanel}`)}</h2>
|
||||
</div>
|
||||
|
||||
<div className="config-panel-body">
|
||||
{activePanel === 'profile' && (
|
||||
<PanelProfile
|
||||
|
||||
@@ -407,30 +407,14 @@ input::placeholder { color: var(--text-disabled); }
|
||||
display: flex; justify-content: flex-end; gap: 8px;
|
||||
}
|
||||
|
||||
.config-window { display: flex; height: 100%; overflow: hidden; }
|
||||
.config-window { display: flex; flex-direction: column; height: 100%; overflow: hidden; }
|
||||
|
||||
.config-sidebar {
|
||||
width: 180px; background: var(--bg-surface); border-right: 1px solid var(--border);
|
||||
display: flex; flex-direction: column; padding: 12px 8px; gap: 2px; flex-shrink: 0;
|
||||
overflow-y: auto;
|
||||
.config-tabs-bar {
|
||||
display: flex; gap: 4px; padding: 12px 20px 0; background: var(--bg-surface);
|
||||
border-bottom: 1px solid var(--border); flex-shrink: 0;
|
||||
}
|
||||
.config-sidebar-item {
|
||||
display: flex; align-items: center; gap: 10px; padding: 9px 12px;
|
||||
border-radius: var(--radius); font-size: 13px; font-weight: 500;
|
||||
color: var(--text-tertiary); cursor: pointer; transition: all 0.15s;
|
||||
user-select: none;
|
||||
}
|
||||
.config-sidebar-item:hover { background: var(--bg-card); color: var(--text-primary); }
|
||||
.config-sidebar-item.active { background: var(--accent-bg); color: var(--accent); font-weight: 600; }
|
||||
.config-sidebar-item svg { flex-shrink: 0; }
|
||||
|
||||
.config-panel-area { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
|
||||
.config-panel-header {
|
||||
padding: 20px 28px 0; flex-shrink: 0;
|
||||
}
|
||||
.config-panel-title {
|
||||
font-size: 16px; font-weight: 700; color: var(--text-primary); margin: 0;
|
||||
}
|
||||
.config-panel-body { flex: 1; overflow-y: auto; padding: 16px 28px 28px; }
|
||||
|
||||
.config-card {
|
||||
@@ -605,3 +589,45 @@ input::placeholder { color: var(--text-disabled); }
|
||||
.feed-content { font-size: 14px; line-height: 1.7; color: var(--text-primary); word-break: break-word; }
|
||||
.feed-system-badge { width: 6px; height: 6px; border-radius: 50%; background: var(--accent-dim); flex-shrink: 0; }
|
||||
.feed-system-text { font-size: 12px; color: var(--text-tertiary); font-style: italic; flex: 1; }
|
||||
|
||||
.studio-code-block {
|
||||
background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius);
|
||||
overflow: hidden; margin: 8px 0;
|
||||
}
|
||||
.studio-code-block pre { padding: 12px 16px; font-family: var(--font-mono); font-size: 13px; line-height: 1.5; overflow-x: auto; color: var(--text-primary); margin: 0; }
|
||||
.studio-code-lang {
|
||||
padding: 4px 12px; font-size: 11px; font-weight: 600; color: var(--text-tertiary);
|
||||
background: var(--bg-surface); border-bottom: 1px solid var(--border); text-transform: uppercase; letter-spacing: 0.5px;
|
||||
}
|
||||
.inline-code { background: var(--bg-input); padding: 2px 6px; border-radius: 4px; font-family: var(--font-mono); font-size: 13px; color: var(--accent-muted); }
|
||||
.msg-h3 { font-size: 16px; font-weight: 700; color: var(--text-primary); margin: 16px 0 8px; display: block; }
|
||||
.msg-h4 { font-size: 14px; font-weight: 700; color: var(--text-secondary); margin: 12px 0 6px; display: block; }
|
||||
.msg-bullet { display: block; padding-left: 16px; position: relative; margin: 2px 0; }
|
||||
.msg-bullet::before { content: '\2022'; position: absolute; left: 4px; color: var(--accent); }
|
||||
.msg-step { display: flex; gap: 8px; align-items: baseline; margin: 3px 0; }
|
||||
.msg-step-num { color: var(--accent); font-weight: 700; font-family: var(--font-mono); font-size: 13px; flex-shrink: 0; min-width: 20px; }
|
||||
.studio-cursor { display: inline-block; width: 8px; height: 16px; background: var(--accent); margin-left: 2px; vertical-align: text-bottom; animation: blink 0.8s step-end infinite; }
|
||||
@keyframes blink { 50% { opacity: 0; } }
|
||||
.studio-thinking { display: flex; gap: 4px; padding: 8px 0; }
|
||||
.studio-thinking span { width: 6px; height: 6px; border-radius: 50%; background: var(--accent-dim); animation: bounce 1.2s ease-in-out infinite; }
|
||||
.studio-thinking span:nth-child(2) { animation-delay: 0.15s; }
|
||||
.studio-thinking span:nth-child(3) { animation-delay: 0.3s; }
|
||||
@keyframes bounce { 0%, 80%, 100% { transform: translateY(0); opacity: 0.4; } 40% { transform: translateY(-6px); opacity: 1; } }
|
||||
.studio-input-area { padding: 12px 20px 8px; border-top: 1px solid var(--border); background: var(--bg-surface); }
|
||||
.studio-input-row { display: flex; gap: 8px; align-items: flex-end; }
|
||||
.studio-input-row textarea {
|
||||
flex: 1; resize: none; min-height: 42px; max-height: 200px; padding: 10px 14px;
|
||||
font-size: 14px; line-height: 1.5; border-radius: var(--radius);
|
||||
background: var(--bg-input); color: var(--text-primary); border: 1px solid var(--border);
|
||||
font-family: var(--font-sans); outline: none; transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
.studio-input-row textarea:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--border-accent); }
|
||||
.studio-input-row textarea::placeholder { color: var(--text-disabled); }
|
||||
.studio-send-btn {
|
||||
width: 42px; height: 42px; padding: 0; display: flex; align-items: center; justify-content: center;
|
||||
border-radius: var(--radius); background: var(--accent); color: #fff; border: 1px solid var(--accent);
|
||||
cursor: pointer; transition: all 0.15s; flex-shrink: 0;
|
||||
}
|
||||
.studio-send-btn:hover:not(:disabled) { background: var(--accent-bright); border-color: var(--accent-bright); }
|
||||
.studio-send-btn:disabled { opacity: 0.3; cursor: not-allowed; }
|
||||
.studio-input-hint { font-size: 11px; color: var(--text-disabled); text-align: center; margin-top: 6px; }
|
||||
|
||||
Reference in New Issue
Block a user