feat: RAG, memory, plugins, lessons, file editor, split panes, Markdown rendering, PWA + UI overhaul
All checks were successful
Stable Release / stable (push) Successful in 1m34s

Major additions:
- RAG pipeline (indexing, chunking, search) with sidebar upload button
- Memory system with CRUD API
- Plugins and lessons modules
- MCP discovery and MCP server
- Advanced skills (auto-create, conditional, improver)
- Agent browser/image support, delegate, sessions
- File editor with CodeMirror in split panes
- Markdown rendering via react-markdown + KaTeX + highlight.js
- Raw markdown toggle
- PWA manifest + service worker
- Extension UI redesign with new design tokens and studio-style chat
- Pipeline API for chat streaming
- Mobile responsive layout

💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
This commit is contained in:
Augustin
2026-04-27 21:01:08 +02:00
parent 62c20eb174
commit 4523bbd42c
50 changed files with 11144 additions and 469 deletions

View File

@@ -0,0 +1,336 @@
package api
import (
"encoding/json"
"fmt"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/muyue/muyue/internal/config"
"github.com/muyue/muyue/internal/mcpserver"
)
func (s *Server) handleFileContent(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
path := r.URL.Query().Get("path")
if path == "" {
writeError(w, "path parameter required", http.StatusBadRequest)
return
}
home, _ := os.UserHomeDir()
path = strings.ReplaceAll(path, "~", home)
if !filepath.IsAbs(path) {
writeError(w, "path must be absolute", http.StatusBadRequest)
return
}
data, err := os.ReadFile(path)
if err != nil {
writeError(w, fmt.Sprintf("Error reading file: %v", err), http.StatusNotFound)
return
}
ext := strings.ToLower(filepath.Ext(path))
lang := "text"
switch ext {
case ".go":
lang = "go"
case ".js", ".jsx":
lang = "javascript"
case ".ts", ".tsx":
lang = "typescript"
case ".py":
lang = "python"
case ".json":
lang = "json"
case ".yaml", ".yml":
lang = "yaml"
case ".md":
lang = "markdown"
case ".css":
lang = "css"
case ".html":
lang = "html"
case ".sh", ".bash":
lang = "shell"
case ".rs":
lang = "rust"
case ".java":
lang = "java"
}
stat, _ := os.Stat(path)
modTime := ""
if stat != nil {
modTime = stat.ModTime().Format("2006-01-02T15:04:05Z07:00")
}
writeJSON(w, map[string]interface{}{
"path": path,
"content": string(data),
"lang": lang,
"size": len(data),
"modTime": modTime,
})
case http.MethodPut:
var body struct {
Path string `json:"path"`
Content string `json:"content"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeError(w, err.Error(), http.StatusBadRequest)
return
}
if body.Path == "" {
writeError(w, "path required", http.StatusBadRequest)
return
}
home, _ := os.UserHomeDir()
path := strings.ReplaceAll(body.Path, "~", home)
if !filepath.IsAbs(path) {
writeError(w, "path must be absolute", http.StatusBadRequest)
return
}
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0755); err != nil {
writeError(w, fmt.Sprintf("Error creating directory: %v", err), http.StatusInternalServerError)
return
}
if err := os.WriteFile(path, []byte(body.Content), 0644); err != nil {
writeError(w, fmt.Sprintf("Error writing file: %v", err), http.StatusInternalServerError)
return
}
writeJSON(w, map[string]interface{}{
"status": "ok",
"path": path,
"size": len(body.Content),
})
default:
writeError(w, "GET/PUT only", http.StatusMethodNotAllowed)
}
}
func (s *Server) handleMuyueMCPServerStatus(w http.ResponseWriter, r *http.Request) {
writeJSON(w, map[string]interface{}{
"enabled": s.mcpServer != nil,
"running": s.mcpServer != nil,
"port": s.getMCPServerPort(),
})
}
func (s *Server) handleMuyueMCPServerStart(w http.ResponseWriter, r *http.Request) {
if s.mcpServer != nil {
writeJSON(w, map[string]string{"status": "already_running"})
return
}
s.startMCPServer()
writeJSON(w, map[string]interface{}{
"status": "started",
"port": s.getMCPServerPort(),
})
}
func (s *Server) handleMuyueMCPServerStop(w http.ResponseWriter, r *http.Request) {
if s.mcpServer == nil {
writeJSON(w, map[string]string{"status": "not_running"})
return
}
s.mcpServer.Stop()
s.mcpServer = nil
writeJSON(w, map[string]string{"status": "stopped"})
}
func (s *Server) getMCPServerPort() int {
if s.mcpServer == nil {
return 0
}
return s.mcpServer.Port()
}
func (s *Server) startMCPServer() {
port := 8096
if s.config != nil {
}
s.mcpServer = mcpserver.New(port)
s.mcpServer.Start()
}
func (s *Server) handleAgentSessionsList(w http.ResponseWriter, r *http.Request) {
sessions := s.agentTracker.Discover()
writeJSON(w, map[string]interface{}{
"sessions": sessions,
})
}
func (s *Server) handleAgentSessionOutput(w http.ResponseWriter, r *http.Request) {
id := strings.TrimPrefix(r.URL.Path, "/api/agent-sessions/")
if id == "" {
writeError(w, "session id required", http.StatusBadRequest)
return
}
session := s.agentTracker.Get(id)
if session == nil {
writeError(w, "session not found", http.StatusNotFound)
return
}
writeJSON(w, session)
}
func (s *Server) handleWorkspaceList(w http.ResponseWriter, r *http.Request) {
dir, err := configWorkspacesDir()
if err != nil {
writeError(w, err.Error(), http.StatusInternalServerError)
return
}
entries, err := os.ReadDir(dir)
if err != nil {
if os.IsNotExist(err) {
writeJSON(w, map[string]interface{}{"workspaces": []interface{}{}})
return
}
writeError(w, err.Error(), http.StatusInternalServerError)
return
}
var workspaces []map[string]interface{}
for _, entry := range entries {
if entry.IsDir() {
continue
}
if !strings.HasSuffix(entry.Name(), ".json") {
continue
}
name := strings.TrimSuffix(entry.Name(), ".json")
data, err := os.ReadFile(filepath.Join(dir, entry.Name()))
if err != nil {
continue
}
var ws map[string]interface{}
if err := json.Unmarshal(data, &ws); err != nil {
continue
}
ws["name"] = name
workspaces = append(workspaces, ws)
}
if workspaces == nil {
workspaces = []map[string]interface{}{}
}
writeJSON(w, map[string]interface{}{"workspaces": workspaces})
}
func (s *Server) handleWorkspaceSave(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
writeError(w, "POST only", http.StatusMethodNotAllowed)
return
}
var body struct {
Name string `json:"name"`
Layout string `json:"layout"`
Tabs string `json:"tabs"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeError(w, err.Error(), http.StatusBadRequest)
return
}
if body.Name == "" {
writeError(w, "name required", http.StatusBadRequest)
return
}
dir, err := configWorkspacesDir()
if err != nil {
writeError(w, err.Error(), http.StatusInternalServerError)
return
}
wsData := map[string]interface{}{
"name": body.Name,
"layout": body.Layout,
"tabs": body.Tabs,
"updated": fmt.Sprintf("%d", time.Now().Unix()),
}
data, err := json.MarshalIndent(wsData, "", " ")
if err != nil {
writeError(w, err.Error(), http.StatusInternalServerError)
return
}
if err := os.WriteFile(filepath.Join(dir, body.Name+".json"), data, 0644); err != nil {
writeError(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, map[string]string{"status": "ok"})
}
func (s *Server) handleWorkspaceGet(w http.ResponseWriter, r *http.Request) {
name := strings.TrimPrefix(r.URL.Path, "/api/workspace/")
if name == "" {
writeError(w, "name required", http.StatusBadRequest)
return
}
if r.Method == "DELETE" {
dir, err := configWorkspacesDir()
if err != nil {
writeError(w, err.Error(), http.StatusInternalServerError)
return
}
if err := os.Remove(filepath.Join(dir, name+".json")); err != nil {
writeError(w, "workspace not found", http.StatusNotFound)
return
}
writeJSON(w, map[string]string{"status": "ok"})
return
}
dir, err := configWorkspacesDir()
if err != nil {
writeError(w, err.Error(), http.StatusInternalServerError)
return
}
data, err := os.ReadFile(filepath.Join(dir, name+".json"))
if err != nil {
writeError(w, "workspace not found", http.StatusNotFound)
return
}
var result map[string]interface{}
json.Unmarshal(data, &result)
writeJSON(w, result)
}
func configWorkspacesDir() (string, error) {
configDir, err := config.ConfigDir()
if err != nil {
return "", err
}
dir := filepath.Join(configDir, "workspaces")
if err := os.MkdirAll(dir, 0755); err != nil {
return "", fmt.Errorf("create workspaces dir: %w", err)
}
return dir, nil
}