chore: remove dead code (packages, functions, types, constants)
All checks were successful
Beta Release / beta (push) Successful in 34s
All checks were successful
Beta Release / beta (push) Successful in 34s
Remove 5 unused packages (daemon, preview, proxy, workflow) and dead symbols across 7 files: orchestrator workflow engine, skills Target type and Update(), LSP config generation, installer SetupPrompt(), unexported desktop options, and version License/Prerelease. Total: -1453 lines.
This commit is contained in:
@@ -1,173 +0,0 @@
|
|||||||
package daemon
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"sync"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/muyue/muyue/internal/config"
|
|
||||||
"github.com/muyue/muyue/internal/scanner"
|
|
||||||
"github.com/muyue/muyue/internal/updater"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Daemon struct {
|
|
||||||
config *config.MuyueConfig
|
|
||||||
interval time.Duration
|
|
||||||
stopCh chan struct{}
|
|
||||||
mu sync.RWMutex
|
|
||||||
running bool
|
|
||||||
lastCheck time.Time
|
|
||||||
lastStatus []updater.UpdateStatus
|
|
||||||
logs []string
|
|
||||||
onUpdate func([]updater.UpdateStatus)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewDaemon(cfg *config.MuyueConfig, interval time.Duration) *Daemon {
|
|
||||||
if interval == 0 {
|
|
||||||
interval = 1 * time.Hour
|
|
||||||
}
|
|
||||||
return &Daemon{
|
|
||||||
config: cfg,
|
|
||||||
interval: interval,
|
|
||||||
stopCh: make(chan struct{}, 1),
|
|
||||||
logs: []string{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Daemon) OnUpdate(fn func([]updater.UpdateStatus)) {
|
|
||||||
d.onUpdate = fn
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Daemon) Start() error {
|
|
||||||
d.mu.Lock()
|
|
||||||
if d.running {
|
|
||||||
d.mu.Unlock()
|
|
||||||
return fmt.Errorf("daemon already running")
|
|
||||||
}
|
|
||||||
d.running = true
|
|
||||||
d.mu.Unlock()
|
|
||||||
|
|
||||||
d.log("daemon started (interval: %s)", d.interval)
|
|
||||||
|
|
||||||
go d.run()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Daemon) Stop() {
|
|
||||||
d.mu.Lock()
|
|
||||||
defer d.mu.Unlock()
|
|
||||||
if !d.running {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
d.running = false
|
|
||||||
d.stopCh <- struct{}{}
|
|
||||||
d.log("daemon stopped")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Daemon) IsRunning() bool {
|
|
||||||
d.mu.RLock()
|
|
||||||
defer d.mu.RUnlock()
|
|
||||||
return d.running
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Daemon) LastCheck() time.Time {
|
|
||||||
d.mu.RLock()
|
|
||||||
defer d.mu.RUnlock()
|
|
||||||
return d.lastCheck
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Daemon) LastStatus() []updater.UpdateStatus {
|
|
||||||
d.mu.RLock()
|
|
||||||
defer d.mu.RUnlock()
|
|
||||||
return d.lastStatus
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Daemon) Logs() []string {
|
|
||||||
d.mu.RLock()
|
|
||||||
defer d.mu.RUnlock()
|
|
||||||
return d.logs
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Daemon) TriggerCheck() []updater.UpdateStatus {
|
|
||||||
return d.checkUpdates()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Daemon) run() {
|
|
||||||
d.checkUpdates()
|
|
||||||
|
|
||||||
ticker := time.NewTicker(d.interval)
|
|
||||||
defer ticker.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ticker.C:
|
|
||||||
d.checkUpdates()
|
|
||||||
case <-d.stopCh:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Daemon) checkUpdates() []updater.UpdateStatus {
|
|
||||||
d.log("checking for updates...")
|
|
||||||
result := scanner.ScanSystem()
|
|
||||||
statuses := updater.CheckUpdates(result)
|
|
||||||
|
|
||||||
needsUpdate := false
|
|
||||||
for _, s := range statuses {
|
|
||||||
if s.NeedsUpdate {
|
|
||||||
needsUpdate = true
|
|
||||||
d.log("update available: %s %s -> %s", s.Tool, s.Current, s.Latest)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !needsUpdate {
|
|
||||||
d.log("all tools up to date")
|
|
||||||
}
|
|
||||||
|
|
||||||
d.mu.Lock()
|
|
||||||
d.lastCheck = time.Now()
|
|
||||||
d.lastStatus = statuses
|
|
||||||
d.mu.Unlock()
|
|
||||||
|
|
||||||
if d.config.Profile.Preferences.AutoUpdate && needsUpdate {
|
|
||||||
d.log("auto-updating...")
|
|
||||||
results := updater.RunAutoUpdate(statuses)
|
|
||||||
for _, r := range results {
|
|
||||||
if r.Message != "" {
|
|
||||||
d.log(" %s: %s", r.Tool, r.Message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if d.onUpdate != nil {
|
|
||||||
d.onUpdate(statuses)
|
|
||||||
}
|
|
||||||
|
|
||||||
return statuses
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Daemon) log(format string, args ...interface{}) {
|
|
||||||
msg := fmt.Sprintf("[%s] %s", time.Now().Format("15:04:05"), fmt.Sprintf(format, args...))
|
|
||||||
d.mu.Lock()
|
|
||||||
d.logs = append(d.logs, msg)
|
|
||||||
if len(d.logs) > 500 {
|
|
||||||
d.logs = d.logs[250:]
|
|
||||||
}
|
|
||||||
d.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func RunStandalone(cfg *config.MuyueConfig) {
|
|
||||||
d := NewDaemon(cfg, 1*time.Hour)
|
|
||||||
d.Start()
|
|
||||||
|
|
||||||
sigCh := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
|
||||||
|
|
||||||
<-sigCh
|
|
||||||
d.Stop()
|
|
||||||
}
|
|
||||||
@@ -26,11 +26,11 @@ type options struct {
|
|||||||
|
|
||||||
type option func(*options)
|
type option func(*options)
|
||||||
|
|
||||||
func WithPort(port int) option {
|
func withPort(port int) option {
|
||||||
return func(o *options) { o.port = port }
|
return func(o *options) { o.port = port }
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithNoOpen(noOpen bool) option {
|
func withNoOpen(noOpen bool) option {
|
||||||
return func(o *options) { o.noOpen = noOpen }
|
return func(o *options) { o.noOpen = noOpen }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,10 +39,10 @@ func parseFlags(args []string) []option {
|
|||||||
for _, arg := range args {
|
for _, arg := range args {
|
||||||
switch {
|
switch {
|
||||||
case arg == "--no-open":
|
case arg == "--no-open":
|
||||||
opts = append(opts, WithNoOpen(true))
|
opts = append(opts, withNoOpen(true))
|
||||||
case strings.HasPrefix(arg, "--port="):
|
case strings.HasPrefix(arg, "--port="):
|
||||||
if p, err := strconv.Atoi(strings.TrimPrefix(arg, "--port=")); err == nil {
|
if p, err := strconv.Atoi(strings.TrimPrefix(arg, "--port=")); err == nil {
|
||||||
opts = append(opts, WithPort(p))
|
opts = append(opts, withPort(p))
|
||||||
}
|
}
|
||||||
case arg == "--port":
|
case arg == "--port":
|
||||||
// handled as prefix case
|
// handled as prefix case
|
||||||
|
|||||||
@@ -290,46 +290,6 @@ func (i *Installer) installGit() InstallResult {
|
|||||||
return InstallResult{Tool: "git", Success: true, Message: "installed and configured"}
|
return InstallResult{Tool: "git", Success: true, Message: "installed and configured"}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Installer) SetupPrompt() error {
|
|
||||||
starshipPath, err := exec.LookPath("starship")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("starship not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
rcFile := i.getRCFile()
|
|
||||||
line := fmt.Sprintf("eval \"$(" + starshipPath + " init %s)\"", i.system.Shell)
|
|
||||||
appendLine(rcFile, line)
|
|
||||||
|
|
||||||
configDir, _ := config.ConfigDir()
|
|
||||||
starshipConfig := `format = """
|
|
||||||
$directory\
|
|
||||||
$git_branch\
|
|
||||||
$git_status\
|
|
||||||
$git_metrics\
|
|
||||||
$nodejs\
|
|
||||||
$python\
|
|
||||||
$golang\
|
|
||||||
$rust\
|
|
||||||
$cmd_duration\
|
|
||||||
$line_break\
|
|
||||||
$character"""
|
|
||||||
|
|
||||||
[character]
|
|
||||||
success_symbol = "[❯](bold green)"
|
|
||||||
error_symbol = "[❯](bold red)"
|
|
||||||
|
|
||||||
[git_branch]
|
|
||||||
format = "[$symbol$branch]($style) "
|
|
||||||
|
|
||||||
[git_status]
|
|
||||||
format = '([$all_status$ahead_behind]($style) )'
|
|
||||||
`
|
|
||||||
configPath := configDir + "/starship.toml"
|
|
||||||
os.MkdirAll(configDir, 0755)
|
|
||||||
os.WriteFile(configPath, []byte(starshipConfig), 0644)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *Installer) getRCFile() string {
|
func (i *Installer) getRCFile() string {
|
||||||
home, _ := os.UserHomeDir()
|
home, _ := os.UserHomeDir()
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
package lsp
|
package lsp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/muyue/muyue/internal/config"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type LSPServer struct {
|
type LSPServer struct {
|
||||||
@@ -15,14 +11,9 @@ type LSPServer struct {
|
|||||||
Language string `json:"language"`
|
Language string `json:"language"`
|
||||||
Command string `json:"command"`
|
Command string `json:"command"`
|
||||||
InstallCmd string `json:"install_cmd"`
|
InstallCmd string `json:"install_cmd"`
|
||||||
ConfigFile string `json:"config_file"`
|
|
||||||
Installed bool `json:"installed"`
|
Installed bool `json:"installed"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LSPConfig struct {
|
|
||||||
Servers []LSPServer `json:"servers"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var knownServers = []LSPServer{
|
var knownServers = []LSPServer{
|
||||||
{Name: "gopls", Language: "go", Command: "gopls", InstallCmd: "go install golang.org/x/tools/gopls@latest"},
|
{Name: "gopls", Language: "go", Command: "gopls", InstallCmd: "go install golang.org/x/tools/gopls@latest"},
|
||||||
{Name: "pyright", Language: "python", Command: "pyright", InstallCmd: "npm install -g pyright"},
|
{Name: "pyright", Language: "python", Command: "pyright", InstallCmd: "npm install -g pyright"},
|
||||||
@@ -111,85 +102,4 @@ func InstallForLanguages(languages []string) []LSPServer {
|
|||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
func GenerateCrushConfig(cfg *config.MuyueConfig) error {
|
|
||||||
if cfg == nil {
|
|
||||||
return fmt.Errorf("config is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
configDir, err := config.ConfigDir()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
type lspEntry struct {
|
|
||||||
Command []string `json:"command"`
|
|
||||||
}
|
|
||||||
|
|
||||||
lspConfig := map[string]lspEntry{}
|
|
||||||
|
|
||||||
for _, lang := range cfg.Profile.Languages {
|
|
||||||
switch lang {
|
|
||||||
case "go":
|
|
||||||
lspConfig["go"] = lspEntry{Command: []string{"gopls"}}
|
|
||||||
case "python":
|
|
||||||
lspConfig["python"] = lspEntry{Command: []string{"pyright-langserver", "--stdio"}}
|
|
||||||
case "typescript", "javascript":
|
|
||||||
lspConfig["typescript"] = lspEntry{Command: []string{"typescript-language-server", "--stdio"}}
|
|
||||||
case "rust":
|
|
||||||
lspConfig["rust"] = lspEntry{Command: []string{"rust-analyzer"}}
|
|
||||||
case "c", "cpp":
|
|
||||||
lspConfig["c"] = lspEntry{Command: []string{"clangd"}}
|
|
||||||
case "lua":
|
|
||||||
lspConfig["lua"] = lspEntry{Command: []string{"lua-language-server"}}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(lspConfig) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := json.MarshalIndent(lspConfig, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
lspPath := filepath.Join(configDir, "crush.json")
|
|
||||||
existing, err := os.ReadFile(lspPath)
|
|
||||||
if err == nil {
|
|
||||||
var existingConfig map[string]interface{}
|
|
||||||
if unmarshalErr := json.Unmarshal(existing, &existingConfig); unmarshalErr == nil {
|
|
||||||
var newConfig map[string]interface{}
|
|
||||||
if unmarshalErr2 := json.Unmarshal(data, &newConfig); unmarshalErr2 == nil {
|
|
||||||
for k, v := range newConfig {
|
|
||||||
existingConfig[k] = v
|
|
||||||
}
|
|
||||||
data, _ = json.MarshalIndent(existingConfig, "", " ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return os.WriteFile(lspPath, data, 0644)
|
|
||||||
}
|
|
||||||
|
|
||||||
func EnsureCrushConfig(cfg *config.MuyueConfig) error {
|
|
||||||
configDir, _ := config.ConfigDir()
|
|
||||||
crusherPath := filepath.Join(configDir, "crush.json")
|
|
||||||
|
|
||||||
if _, err := os.Stat(crusherPath); err != nil {
|
|
||||||
home, _ := os.UserHomeDir()
|
|
||||||
homeCrush := filepath.Join(home, ".config", "crush", "crush.json")
|
|
||||||
if _, err := os.Stat(homeCrush); err == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultConfig := map[string]interface{}{
|
|
||||||
"version": "1",
|
|
||||||
}
|
|
||||||
|
|
||||||
data, _ := json.MarshalIndent(defaultConfig, "", " ")
|
|
||||||
os.MkdirAll(filepath.Dir(crusherPath), 0755)
|
|
||||||
return os.WriteFile(crusherPath, data, 0644)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/muyue/muyue/internal/config"
|
"github.com/muyue/muyue/internal/config"
|
||||||
"github.com/muyue/muyue/internal/workflow"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var thinkRegex = regexp.MustCompile(`(?s)<[Tt]hink[^>]*>.*?</[Tt]hink>`)
|
var thinkRegex = regexp.MustCompile(`(?s)<[Tt]hink[^>]*>.*?</[Tt]hink>`)
|
||||||
@@ -47,7 +46,6 @@ type Orchestrator struct {
|
|||||||
client *http.Client
|
client *http.Client
|
||||||
history []Message
|
history []Message
|
||||||
histMu sync.Mutex
|
histMu sync.Mutex
|
||||||
Workflow *workflow.Workflow
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var sharedHTTPClient = &http.Client{
|
var sharedHTTPClient = &http.Client{
|
||||||
@@ -72,11 +70,10 @@ func New(cfg *config.MuyueConfig) (*Orchestrator, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &Orchestrator{
|
return &Orchestrator{
|
||||||
config: cfg,
|
config: cfg,
|
||||||
provider: provider,
|
provider: provider,
|
||||||
client: sharedHTTPClient,
|
client: sharedHTTPClient,
|
||||||
history: []Message{},
|
history: []Message{},
|
||||||
Workflow: workflow.New(),
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,156 +150,6 @@ func (o *Orchestrator) Send(userMessage string) (string, error) {
|
|||||||
return content, nil
|
return content, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *Orchestrator) StartWorkflow(goal string) (string, error) {
|
|
||||||
o.Workflow.Start(goal)
|
|
||||||
prompt := fmt.Sprintf("I want to: %s\nWhat questions do you need to ask me to fully understand this requirement? Ask ALL questions at once.", goal)
|
|
||||||
o.history = []Message{
|
|
||||||
{Role: "system", Content: workflow.BuildSystemPrompt(workflow.PhaseGathering, o.Workflow.Plan)},
|
|
||||||
{Role: "user", Content: prompt},
|
|
||||||
}
|
|
||||||
|
|
||||||
reqBody := ChatRequest{
|
|
||||||
Model: o.provider.Model,
|
|
||||||
Messages: o.history,
|
|
||||||
Stream: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := json.Marshal(reqBody)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("marshal request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
baseURL := o.provider.BaseURL
|
|
||||||
if baseURL == "" {
|
|
||||||
baseURL = getProviderBaseURL(o.provider.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
url := strings.TrimRight(baseURL, "/") + "/chat/completions"
|
|
||||||
|
|
||||||
req, err := http.NewRequest("POST", url, bytes.NewReader(body))
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("create request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
req.Header.Set("Authorization", "Bearer "+o.provider.APIKey)
|
|
||||||
|
|
||||||
resp, err := o.client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("send request: %w", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
respBody, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("read response: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return "", fmt.Errorf("API error (%d): %s", resp.StatusCode, string(respBody))
|
|
||||||
}
|
|
||||||
|
|
||||||
var chatResp ChatResponse
|
|
||||||
if err := json.Unmarshal(respBody, &chatResp); err != nil {
|
|
||||||
return "", fmt.Errorf("parse response: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(chatResp.Choices) == 0 {
|
|
||||||
return "", fmt.Errorf("no response from AI")
|
|
||||||
}
|
|
||||||
|
|
||||||
content := cleanAIResponse(chatResp.Choices[0].Message.Content)
|
|
||||||
o.history = append(o.history, Message{
|
|
||||||
Role: "assistant",
|
|
||||||
Content: content,
|
|
||||||
})
|
|
||||||
|
|
||||||
return content, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *Orchestrator) AnswerQuestion(answer string) (string, error) {
|
|
||||||
o.Workflow.AddAnswer(answer)
|
|
||||||
return o.Send(answer)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *Orchestrator) GeneratePlan() (string, error) {
|
|
||||||
o.Workflow.Phase = workflow.PhasePlanning
|
|
||||||
o.history = append(o.history, Message{
|
|
||||||
Role: "system",
|
|
||||||
Content: workflow.BuildSystemPrompt(workflow.PhasePlanning, o.Workflow.Plan),
|
|
||||||
})
|
|
||||||
|
|
||||||
prompt := "All questions have been answered. Now create a detailed step-by-step execution plan as a JSON array. Each step should have: id, title, description, agent (crush/claude/muyue)."
|
|
||||||
if len(o.Workflow.Plan.PreviewFiles) > 0 {
|
|
||||||
prompt += "\nInclude visual previews where helpful using the PREVIEW_JSON format."
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := o.Send(prompt)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
steps, parseErr := workflow.ParsePlanResponse(resp)
|
|
||||||
if parseErr == nil {
|
|
||||||
o.Workflow.SetPlan("")
|
|
||||||
o.Workflow.Plan.Steps = steps
|
|
||||||
o.Workflow.Phase = workflow.PhaseReviewing
|
|
||||||
}
|
|
||||||
|
|
||||||
previewFiles := workflow.ParsePreviewFiles(resp)
|
|
||||||
if len(previewFiles) > 0 {
|
|
||||||
o.Workflow.SetPreviewFiles(previewFiles)
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *Orchestrator) ReviewPlan(approved bool, feedback string) (string, error) {
|
|
||||||
if approved {
|
|
||||||
o.Workflow.Approve()
|
|
||||||
return o.executeNextStep()
|
|
||||||
}
|
|
||||||
o.Workflow.Reject(feedback)
|
|
||||||
return o.Send(fmt.Sprintf("The plan was rejected. Reason: %s. Please revise the plan.", feedback))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *Orchestrator) executeNextStep() (string, error) {
|
|
||||||
step := o.Workflow.CurrentStep()
|
|
||||||
if step == nil {
|
|
||||||
return "All steps completed!", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
o.history = append(o.history, Message{
|
|
||||||
Role: "system",
|
|
||||||
Content: workflow.BuildSystemPrompt(workflow.PhaseExecuting, o.Workflow.Plan),
|
|
||||||
})
|
|
||||||
|
|
||||||
return o.Send(fmt.Sprintf("Execute step %s: %s\n%s", step.ID, step.Title, step.Description))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *Orchestrator) ContinueExecution(output string) (string, error) {
|
|
||||||
o.Workflow.AdvanceStep(output)
|
|
||||||
if o.Workflow.Phase == workflow.PhaseDone {
|
|
||||||
return "Workflow completed! All steps have been executed.", nil
|
|
||||||
}
|
|
||||||
return o.executeNextStep()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *Orchestrator) History() []Message {
|
|
||||||
o.histMu.Lock()
|
|
||||||
defer o.histMu.Unlock()
|
|
||||||
cp := make([]Message, len(o.history))
|
|
||||||
copy(cp, o.history)
|
|
||||||
return cp
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *Orchestrator) ClearHistory() {
|
|
||||||
o.histMu.Lock()
|
|
||||||
o.history = []Message{}
|
|
||||||
o.histMu.Unlock()
|
|
||||||
o.Workflow.Reset()
|
|
||||||
}
|
|
||||||
|
|
||||||
func cleanAIResponse(content string) string {
|
func cleanAIResponse(content string) string {
|
||||||
content = thinkRegex.ReplaceAllString(content, "")
|
content = thinkRegex.ReplaceAllString(content, "")
|
||||||
lines := strings.Split(content, "\n")
|
lines := strings.Split(content, "\n")
|
||||||
|
|||||||
@@ -7,13 +7,6 @@ import (
|
|||||||
"github.com/muyue/muyue/internal/config"
|
"github.com/muyue/muyue/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func testConfig() *config.MuyueConfig {
|
|
||||||
cfg := config.Default()
|
|
||||||
cfg.AI.Providers[0].Active = true
|
|
||||||
cfg.AI.Providers[0].APIKey = "test-api-key-12345"
|
|
||||||
return cfg
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCleanAIResponse(t *testing.T) {
|
func TestCleanAIResponse(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -153,58 +146,3 @@ func TestNewNoAPIKey(t *testing.T) {
|
|||||||
t.Error("Should fail with no API key")
|
t.Error("Should fail with no API key")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHistoryManagement(t *testing.T) {
|
|
||||||
cfg := testConfig()
|
|
||||||
orch, err := New(cfg)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("New failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
h := orch.History()
|
|
||||||
if len(h) != 0 {
|
|
||||||
t.Errorf("Expected empty history, got %d", len(h))
|
|
||||||
}
|
|
||||||
|
|
||||||
orch.ClearHistory()
|
|
||||||
h = orch.History()
|
|
||||||
if len(h) != 0 {
|
|
||||||
t.Errorf("Expected 0 after clear, got %d", len(h))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHistoryCopy(t *testing.T) {
|
|
||||||
cfg := testConfig()
|
|
||||||
orch, _ := New(cfg)
|
|
||||||
|
|
||||||
orch.history = []Message{
|
|
||||||
{Role: "user", Content: "hello"},
|
|
||||||
}
|
|
||||||
|
|
||||||
h := orch.History()
|
|
||||||
h[0].Content = "modified"
|
|
||||||
|
|
||||||
orig := orch.History()
|
|
||||||
if orig[0].Content == "modified" {
|
|
||||||
t.Error("History should return a copy")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMaxHistorySize(t *testing.T) {
|
|
||||||
cfg := testConfig()
|
|
||||||
orch, _ := New(cfg)
|
|
||||||
|
|
||||||
for i := 0; i < maxHistorySize+10; i++ {
|
|
||||||
orch.histMu.Lock()
|
|
||||||
orch.history = append(orch.history, Message{Role: "user", Content: "msg"})
|
|
||||||
if len(orch.history) > maxHistorySize {
|
|
||||||
orch.history = orch.history[len(orch.history)-maxHistorySize:]
|
|
||||||
}
|
|
||||||
orch.histMu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
h := orch.History()
|
|
||||||
if len(h) > maxHistorySize {
|
|
||||||
t.Errorf("History should be capped at %d, got %d", maxHistorySize, len(h))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
package preview
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type PreviewServer struct {
|
|
||||||
dir string
|
|
||||||
server *http.Server
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewPreviewServer(dir string) *PreviewServer {
|
|
||||||
return &PreviewServer{dir: dir}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PreviewServer) Start(port int) error {
|
|
||||||
fs := http.FileServer(http.Dir(p.dir))
|
|
||||||
mux := http.NewServeMux()
|
|
||||||
mux.Handle("/", fs)
|
|
||||||
|
|
||||||
p.server = &http.Server{
|
|
||||||
Addr: fmt.Sprintf("127.0.0.1:%d", port),
|
|
||||||
Handler: mux,
|
|
||||||
ReadTimeout: 30 * time.Second,
|
|
||||||
WriteTimeout: 30 * time.Second,
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
if err := p.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
||||||
fmt.Printf("Preview server error: %s\n", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
url := fmt.Sprintf("http://127.0.0.1:%d", port)
|
|
||||||
fmt.Printf("Preview server running at %s\n", url)
|
|
||||||
|
|
||||||
return openBrowser(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PreviewServer) Stop() error {
|
|
||||||
if p.server != nil {
|
|
||||||
return p.server.Close()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func CreatePreviewFile(dir, filename, content string) error {
|
|
||||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return os.WriteFile(filepath.Join(dir, filename), []byte(content), 0644)
|
|
||||||
}
|
|
||||||
|
|
||||||
func openBrowser(url string) error {
|
|
||||||
var cmd string
|
|
||||||
var args []string
|
|
||||||
|
|
||||||
switch runtime.GOOS {
|
|
||||||
case "linux":
|
|
||||||
cmd = "xdg-open"
|
|
||||||
args = []string{url}
|
|
||||||
case "darwin":
|
|
||||||
cmd = "open"
|
|
||||||
args = []string{url}
|
|
||||||
case "windows":
|
|
||||||
cmd = "cmd"
|
|
||||||
args = []string{"/c", "start", url}
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unsupported platform")
|
|
||||||
}
|
|
||||||
|
|
||||||
return exec.Command(cmd, args...).Start()
|
|
||||||
}
|
|
||||||
@@ -1,250 +0,0 @@
|
|||||||
package proxy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type AgentType string
|
|
||||||
|
|
||||||
const (
|
|
||||||
AgentCrush AgentType = "crush"
|
|
||||||
AgentClaude AgentType = "claude"
|
|
||||||
)
|
|
||||||
|
|
||||||
type AgentStatus string
|
|
||||||
|
|
||||||
const (
|
|
||||||
StatusIdle AgentStatus = "idle"
|
|
||||||
StatusRunning AgentStatus = "running"
|
|
||||||
StatusStopped AgentStatus = "stopped"
|
|
||||||
StatusError AgentStatus = "error"
|
|
||||||
)
|
|
||||||
|
|
||||||
type LogEntry struct {
|
|
||||||
Timestamp time.Time
|
|
||||||
Agent AgentType
|
|
||||||
Level string
|
|
||||||
Message string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Agent struct {
|
|
||||||
Type AgentType
|
|
||||||
Status AgentStatus
|
|
||||||
cmd *exec.Cmd
|
|
||||||
stdout io.Reader
|
|
||||||
stderr io.Reader
|
|
||||||
cancel context.CancelFunc
|
|
||||||
mu sync.Mutex
|
|
||||||
logs []LogEntry
|
|
||||||
}
|
|
||||||
|
|
||||||
type Manager struct {
|
|
||||||
agents map[AgentType]*Agent
|
|
||||||
mu sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewManager() *Manager {
|
|
||||||
return &Manager{
|
|
||||||
agents: make(map[AgentType]*Agent),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) Start(agentType AgentType, args ...string) error {
|
|
||||||
m.mu.Lock()
|
|
||||||
defer m.mu.Unlock()
|
|
||||||
|
|
||||||
if a, exists := m.agents[agentType]; exists && a.Status == StatusRunning {
|
|
||||||
return fmt.Errorf("%s already running", agentType)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
|
|
||||||
var cmdName string
|
|
||||||
switch agentType {
|
|
||||||
case AgentCrush:
|
|
||||||
cmdName = "crush"
|
|
||||||
case AgentClaude:
|
|
||||||
cmdName = "claude"
|
|
||||||
default:
|
|
||||||
cancel()
|
|
||||||
return fmt.Errorf("unknown agent type: %s", agentType)
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := exec.CommandContext(ctx, cmdName, args...)
|
|
||||||
cmd.Env = os.Environ()
|
|
||||||
|
|
||||||
stdout, pipeErr := cmd.StdoutPipe()
|
|
||||||
if pipeErr != nil {
|
|
||||||
cancel()
|
|
||||||
return fmt.Errorf("stdout pipe: %w", pipeErr)
|
|
||||||
}
|
|
||||||
stderr, pipeErr := cmd.StderrPipe()
|
|
||||||
if pipeErr != nil {
|
|
||||||
cancel()
|
|
||||||
return fmt.Errorf("stderr pipe: %w", pipeErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
agent := &Agent{
|
|
||||||
Type: agentType,
|
|
||||||
Status: StatusRunning,
|
|
||||||
cmd: cmd,
|
|
||||||
stdout: stdout,
|
|
||||||
stderr: stderr,
|
|
||||||
cancel: cancel,
|
|
||||||
}
|
|
||||||
|
|
||||||
m.agents[agentType] = agent
|
|
||||||
|
|
||||||
go agent.captureOutput(stdout, "info")
|
|
||||||
go agent.captureOutput(stderr, "error")
|
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
agent.Status = StatusError
|
|
||||||
cancel()
|
|
||||||
return fmt.Errorf("start %s: %w", agentType, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
err := cmd.Wait()
|
|
||||||
m.mu.Lock()
|
|
||||||
defer m.mu.Unlock()
|
|
||||||
if err != nil && ctx.Err() == nil {
|
|
||||||
agent.Status = StatusError
|
|
||||||
agent.log("error", fmt.Sprintf("exited with error: %s", err))
|
|
||||||
} else {
|
|
||||||
agent.Status = StatusStopped
|
|
||||||
agent.log("info", "stopped")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) Stop(agentType AgentType) error {
|
|
||||||
m.mu.Lock()
|
|
||||||
defer m.mu.Unlock()
|
|
||||||
|
|
||||||
agent, exists := m.agents[agentType]
|
|
||||||
if !exists {
|
|
||||||
return fmt.Errorf("%s not found", agentType)
|
|
||||||
}
|
|
||||||
|
|
||||||
if agent.Status != StatusRunning {
|
|
||||||
return fmt.Errorf("%s is not running", agentType)
|
|
||||||
}
|
|
||||||
|
|
||||||
agent.cancel()
|
|
||||||
agent.Status = StatusStopped
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) Status(agentType AgentType) (AgentStatus, []LogEntry) {
|
|
||||||
m.mu.RLock()
|
|
||||||
defer m.mu.RUnlock()
|
|
||||||
|
|
||||||
agent, exists := m.agents[agentType]
|
|
||||||
if !exists {
|
|
||||||
return StatusIdle, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
agent.mu.Lock()
|
|
||||||
defer agent.mu.Unlock()
|
|
||||||
|
|
||||||
return agent.Status, agent.logs
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) AllStatus() map[AgentType]AgentStatus {
|
|
||||||
m.mu.RLock()
|
|
||||||
defer m.mu.RUnlock()
|
|
||||||
|
|
||||||
statuses := make(map[AgentType]AgentStatus)
|
|
||||||
for t, a := range m.agents {
|
|
||||||
statuses[t] = a.Status
|
|
||||||
}
|
|
||||||
return statuses
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func (a *Agent) captureOutput(reader io.Reader, level string) {
|
|
||||||
scanner := bufio.NewScanner(reader)
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := scanner.Text()
|
|
||||||
a.mu.Lock()
|
|
||||||
a.logs = append(a.logs, LogEntry{
|
|
||||||
Timestamp: time.Now(),
|
|
||||||
Agent: a.Type,
|
|
||||||
Level: level,
|
|
||||||
Message: line,
|
|
||||||
})
|
|
||||||
if len(a.logs) > 1000 {
|
|
||||||
a.logs = a.logs[500:]
|
|
||||||
}
|
|
||||||
a.mu.Unlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Agent) log(level, msg string) {
|
|
||||||
a.mu.Lock()
|
|
||||||
defer a.mu.Unlock()
|
|
||||||
a.logs = append(a.logs, LogEntry{
|
|
||||||
Timestamp: time.Now(),
|
|
||||||
Agent: a.Type,
|
|
||||||
Level: level,
|
|
||||||
Message: msg,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) IsAvailable(agentType AgentType) bool {
|
|
||||||
var cmdName string
|
|
||||||
switch agentType {
|
|
||||||
case AgentCrush:
|
|
||||||
cmdName = "crush"
|
|
||||||
case AgentClaude:
|
|
||||||
cmdName = "claude"
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
path, err := exec.LookPath(cmdName)
|
|
||||||
return err == nil && path != ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) GetLogs(agentType AgentType, lastN int) []LogEntry {
|
|
||||||
m.mu.RLock()
|
|
||||||
agent, exists := m.agents[agentType]
|
|
||||||
m.mu.RUnlock()
|
|
||||||
|
|
||||||
if !exists {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
agent.mu.Lock()
|
|
||||||
defer agent.mu.Unlock()
|
|
||||||
|
|
||||||
logs := agent.logs
|
|
||||||
if lastN > 0 && len(logs) > lastN {
|
|
||||||
logs = logs[len(logs)-lastN:]
|
|
||||||
}
|
|
||||||
return logs
|
|
||||||
}
|
|
||||||
|
|
||||||
func FormatLogs(logs []LogEntry) string {
|
|
||||||
var b strings.Builder
|
|
||||||
for _, l := range logs {
|
|
||||||
b.WriteString(fmt.Sprintf("[%s] %s %s: %s\n",
|
|
||||||
l.Timestamp.Format("15:04:05"),
|
|
||||||
l.Agent,
|
|
||||||
l.Level,
|
|
||||||
l.Message,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
@@ -24,14 +24,6 @@ type Skill struct {
|
|||||||
FilePath string `yaml:"-" json:"-"`
|
FilePath string `yaml:"-" json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Target string
|
|
||||||
|
|
||||||
const (
|
|
||||||
TargetCrush Target = "crush"
|
|
||||||
TargetClaude Target = "claude"
|
|
||||||
TargetBoth Target = "both"
|
|
||||||
)
|
|
||||||
|
|
||||||
func SkillsDir() (string, error) {
|
func SkillsDir() (string, error) {
|
||||||
home, err := os.UserHomeDir()
|
home, err := os.UserHomeDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -122,27 +114,6 @@ func Create(skill *Skill) error {
|
|||||||
return Deploy(skill)
|
return Deploy(skill)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Update(skill *Skill) error {
|
|
||||||
dir, err := SkillsDir()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
skillDir := filepath.Join(dir, skill.Name)
|
|
||||||
skillPath := filepath.Join(skillDir, "SKILL.md")
|
|
||||||
if _, err := os.Stat(skillPath); err != nil {
|
|
||||||
return fmt.Errorf("skill '%s' not found", skill.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
skill.UpdatedAt = time.Now()
|
|
||||||
content := renderSkill(skill)
|
|
||||||
if err := os.WriteFile(skillPath, []byte(content), 0644); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return Deploy(skill)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Delete(name string) error {
|
func Delete(name string) error {
|
||||||
dir, err := SkillsDir()
|
dir, err := SkillsDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -164,7 +135,7 @@ func Deploy(skill *Skill) error {
|
|||||||
return fmt.Errorf("get home dir: %w", err)
|
return fmt.Errorf("get home dir: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if skill.Target == string(TargetCrush) || skill.Target == string(TargetBoth) {
|
if skill.Target == "crush" || skill.Target == "both" {
|
||||||
crushSkillsDir := filepath.Join(home, ".config", "crush", "skills")
|
crushSkillsDir := filepath.Join(home, ".config", "crush", "skills")
|
||||||
if err := os.MkdirAll(crushSkillsDir, 0755); err != nil {
|
if err := os.MkdirAll(crushSkillsDir, 0755); err != nil {
|
||||||
return fmt.Errorf("create crush skills dir: %w", err)
|
return fmt.Errorf("create crush skills dir: %w", err)
|
||||||
@@ -179,7 +150,7 @@ func Deploy(skill *Skill) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if skill.Target == string(TargetClaude) || skill.Target == string(TargetBoth) {
|
if skill.Target == "claude" || skill.Target == "both" {
|
||||||
claudeSkillsDir := filepath.Join(home, ".claude", "skills")
|
claudeSkillsDir := filepath.Join(home, ".claude", "skills")
|
||||||
if err := os.MkdirAll(claudeSkillsDir, 0755); err != nil {
|
if err := os.MkdirAll(claudeSkillsDir, 0755); err != nil {
|
||||||
return fmt.Errorf("create claude skills dir: %w", err)
|
return fmt.Errorf("create claude skills dir: %w", err)
|
||||||
|
|||||||
@@ -4,15 +4,8 @@ const (
|
|||||||
Name = "muyue"
|
Name = "muyue"
|
||||||
Version = "0.2.1"
|
Version = "0.2.1"
|
||||||
Author = "La Légion de Muyue"
|
Author = "La Légion de Muyue"
|
||||||
License = "MIT"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var Prerelease string
|
|
||||||
|
|
||||||
func FullVersion() string {
|
func FullVersion() string {
|
||||||
v := Name + " v" + Version
|
return Name + " v" + Version
|
||||||
if Prerelease != "" {
|
|
||||||
v += "-" + Prerelease
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,28 +15,6 @@ func TestFullVersion(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFullVersionWithPrerelease(t *testing.T) {
|
|
||||||
original := Prerelease
|
|
||||||
Prerelease = "beta.1"
|
|
||||||
defer func() { Prerelease = original }()
|
|
||||||
|
|
||||||
v := FullVersion()
|
|
||||||
if !strings.Contains(v, "beta.1") {
|
|
||||||
t.Errorf("FullVersion should contain prerelease suffix, got %s", v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFullVersionWithoutPrerelease(t *testing.T) {
|
|
||||||
original := Prerelease
|
|
||||||
Prerelease = ""
|
|
||||||
defer func() { Prerelease = original }()
|
|
||||||
|
|
||||||
v := FullVersion()
|
|
||||||
if strings.Contains(v, "-") {
|
|
||||||
t.Errorf("FullVersion should not contain prerelease suffix, got %s", v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConstants(t *testing.T) {
|
func TestConstants(t *testing.T) {
|
||||||
if Name == "" {
|
if Name == "" {
|
||||||
t.Error("Name should not be empty")
|
t.Error("Name should not be empty")
|
||||||
@@ -47,7 +25,4 @@ func TestConstants(t *testing.T) {
|
|||||||
if Author == "" {
|
if Author == "" {
|
||||||
t.Error("Author should not be empty")
|
t.Error("Author should not be empty")
|
||||||
}
|
}
|
||||||
if License == "" {
|
|
||||||
t.Error("License should not be empty")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,280 +0,0 @@
|
|||||||
package workflow
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Phase string
|
|
||||||
|
|
||||||
const (
|
|
||||||
PhaseIdle Phase = "idle"
|
|
||||||
PhaseGathering Phase = "gathering"
|
|
||||||
PhasePlanning Phase = "planning"
|
|
||||||
PhaseReviewing Phase = "reviewing"
|
|
||||||
PhaseExecuting Phase = "executing"
|
|
||||||
PhaseDone Phase = "done"
|
|
||||||
PhaseError Phase = "error"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Step struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
Agent string `json:"agent"`
|
|
||||||
Output string `json:"output,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Plan struct {
|
|
||||||
Goal string `json:"goal"`
|
|
||||||
Context string `json:"context"`
|
|
||||||
Questions []string `json:"questions"`
|
|
||||||
Answers []string `json:"answers"`
|
|
||||||
Steps []Step `json:"steps"`
|
|
||||||
StepIndex int `json:"current_step"`
|
|
||||||
PreviewFiles []PreviewFile `json:"preview_files,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PreviewFile struct {
|
|
||||||
Filename string `json:"filename"`
|
|
||||||
Content string `json:"content"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Workflow struct {
|
|
||||||
Phase Phase
|
|
||||||
Plan *Plan
|
|
||||||
History []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func New() *Workflow {
|
|
||||||
return &Workflow{
|
|
||||||
Phase: PhaseIdle,
|
|
||||||
Plan: &Plan{},
|
|
||||||
History: []string{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Workflow) Start(goal string) {
|
|
||||||
w.Phase = PhaseGathering
|
|
||||||
w.Plan = &Plan{
|
|
||||||
Goal: goal,
|
|
||||||
Steps: []Step{},
|
|
||||||
Answers: []string{},
|
|
||||||
}
|
|
||||||
w.History = append(w.History, fmt.Sprintf("[started] %s", goal))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Workflow) AddAnswer(answer string) {
|
|
||||||
w.Plan.Answers = append(w.Plan.Answers, answer)
|
|
||||||
if len(w.Plan.Answers) >= len(w.Plan.Questions) {
|
|
||||||
w.Phase = PhasePlanning
|
|
||||||
w.History = append(w.History, "[gathering complete, moving to planning]")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Workflow) SetPlan(planJSON string) error {
|
|
||||||
var steps []Step
|
|
||||||
if err := json.Unmarshal([]byte(planJSON), &steps); err != nil {
|
|
||||||
if err2 := json.Unmarshal([]byte("["+planJSON+"]"), &steps); err2 != nil {
|
|
||||||
return fmt.Errorf("parse plan: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
w.Plan.Steps = steps
|
|
||||||
w.Phase = PhaseReviewing
|
|
||||||
w.History = append(w.History, fmt.Sprintf("[plan created] %d steps", len(steps)))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Workflow) SetPreviewFiles(files []PreviewFile) {
|
|
||||||
w.Plan.PreviewFiles = files
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Workflow) Approve() {
|
|
||||||
w.Phase = PhaseExecuting
|
|
||||||
w.Plan.StepIndex = 0
|
|
||||||
w.History = append(w.History, "[plan approved, starting execution]")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Workflow) Reject(feedback string) {
|
|
||||||
w.Phase = PhasePlanning
|
|
||||||
w.History = append(w.History, fmt.Sprintf("[plan rejected: %s]", feedback))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Workflow) AdvanceStep(output string) {
|
|
||||||
if w.Plan.StepIndex < len(w.Plan.Steps) {
|
|
||||||
w.Plan.Steps[w.Plan.StepIndex].Status = "done"
|
|
||||||
w.Plan.Steps[w.Plan.StepIndex].Output = output
|
|
||||||
w.Plan.StepIndex++
|
|
||||||
w.History = append(w.History, fmt.Sprintf("[step %d done]", w.Plan.StepIndex))
|
|
||||||
|
|
||||||
if w.Plan.StepIndex >= len(w.Plan.Steps) {
|
|
||||||
w.Phase = PhaseDone
|
|
||||||
w.History = append(w.History, "[all steps complete]")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Workflow) FailStep(errMsg string) {
|
|
||||||
if w.Plan.StepIndex < len(w.Plan.Steps) {
|
|
||||||
w.Plan.Steps[w.Plan.StepIndex].Status = "error"
|
|
||||||
w.Plan.Steps[w.Plan.StepIndex].Output = errMsg
|
|
||||||
w.Phase = PhaseError
|
|
||||||
w.History = append(w.History, fmt.Sprintf("[step %d failed: %s]", w.Plan.StepIndex+1, errMsg))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Workflow) Reset() {
|
|
||||||
w.Phase = PhaseIdle
|
|
||||||
w.Plan = &Plan{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Workflow) CurrentStep() *Step {
|
|
||||||
if w.Plan.StepIndex < len(w.Plan.Steps) {
|
|
||||||
return &w.Plan.Steps[w.Plan.StepIndex]
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Workflow) Progress() (done, total int) {
|
|
||||||
for _, s := range w.Plan.Steps {
|
|
||||||
if s.Status == "done" {
|
|
||||||
done++
|
|
||||||
}
|
|
||||||
total++
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func BuildSystemPrompt(phase Phase, plan *Plan) string {
|
|
||||||
base := `You are muyue, an AI-powered development environment assistant.
|
|
||||||
You follow a structured workflow: GATHER requirements → PLAN → REVIEW → EXECUTE.
|
|
||||||
|
|
||||||
RULES:
|
|
||||||
- Always respond in the same language the user writes in.
|
|
||||||
- When in GATHERING phase, ask clarifying questions ONE AT A TIME to understand the requirement fully.
|
|
||||||
- When in PLANNING phase, create a detailed step-by-step plan as a JSON array of objects.
|
|
||||||
- When in REVIEWING phase, present the plan clearly and wait for approval.
|
|
||||||
- When in EXECUTING phase, execute one step at a time and report results.
|
|
||||||
- If the user wants a visual preview, generate 1-2 HTML files wrapped in a PREVIEW_JSON block.`
|
|
||||||
|
|
||||||
switch phase {
|
|
||||||
case PhaseGathering:
|
|
||||||
base += fmt.Sprintf(`
|
|
||||||
|
|
||||||
CURRENT PHASE: GATHERING
|
|
||||||
Goal: %s
|
|
||||||
Questions to ask: %v
|
|
||||||
Answers received: %v
|
|
||||||
Remaining questions: %d
|
|
||||||
Ask the NEXT question that hasn't been answered yet. If all questions are answered, say "GATHERING_COMPLETE".`,
|
|
||||||
plan.Goal, plan.Questions, plan.Answers,
|
|
||||||
len(plan.Questions)-len(plan.Answers))
|
|
||||||
|
|
||||||
case PhasePlanning:
|
|
||||||
qa := ""
|
|
||||||
for i, q := range plan.Questions {
|
|
||||||
a := ""
|
|
||||||
if i < len(plan.Answers) {
|
|
||||||
a = plan.Answers[i]
|
|
||||||
}
|
|
||||||
qa += fmt.Sprintf("\nQ: %s\nA: %s", q, a)
|
|
||||||
}
|
|
||||||
base += fmt.Sprintf(`
|
|
||||||
|
|
||||||
CURRENT PHASE: PLANNING
|
|
||||||
Goal: %s
|
|
||||||
%s
|
|
||||||
|
|
||||||
Create a step-by-step plan. Output ONLY a JSON array of steps:
|
|
||||||
[
|
|
||||||
{"id": "1", "title": "...", "description": "...", "agent": "crush|claude|muyue", "status": "pending"},
|
|
||||||
...
|
|
||||||
]
|
|
||||||
|
|
||||||
If the user needs a visual preview, wrap HTML in:
|
|
||||||
<<<PREVIEW_JSON>>>
|
|
||||||
[{"filename":"preview.html","content":"<html>...</html>","type":"html"}]
|
|
||||||
<<<END_PREVIEW>>>`,
|
|
||||||
plan.Goal, qa)
|
|
||||||
|
|
||||||
case PhaseReviewing:
|
|
||||||
steps, _ := json.MarshalIndent(plan.Steps, "", " ")
|
|
||||||
base += fmt.Sprintf(`
|
|
||||||
|
|
||||||
CURRENT PHASE: REVIEWING
|
|
||||||
Present the plan below clearly and ask for approval:
|
|
||||||
%s
|
|
||||||
|
|
||||||
Say "PLAN_APPROVED" if the user approves, or "PLAN_REJECTED: <reason>" if not.`,
|
|
||||||
string(steps))
|
|
||||||
|
|
||||||
case PhaseExecuting:
|
|
||||||
if plan.StepIndex < len(plan.Steps) {
|
|
||||||
step := plan.Steps[plan.StepIndex]
|
|
||||||
base += fmt.Sprintf(`
|
|
||||||
|
|
||||||
CURRENT PHASE: EXECUTING
|
|
||||||
Current step: %s — %s (agent: %s)
|
|
||||||
Execute this step and report the result.`,
|
|
||||||
step.Title, step.Description, step.Agent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return base
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParsePlanResponse(response string) ([]Step, error) {
|
|
||||||
response = strings.TrimSpace(response)
|
|
||||||
|
|
||||||
start := strings.Index(response, "[")
|
|
||||||
end := strings.LastIndex(response, "]")
|
|
||||||
if start == -1 || end == -1 || end <= start {
|
|
||||||
return nil, fmt.Errorf("no JSON array found in response")
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonStr := response[start : end+1]
|
|
||||||
var steps []Step
|
|
||||||
if err := json.Unmarshal([]byte(jsonStr), &steps); err != nil {
|
|
||||||
return nil, fmt.Errorf("parse steps: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range steps {
|
|
||||||
steps[i].Status = "pending"
|
|
||||||
}
|
|
||||||
|
|
||||||
return steps, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParsePreviewFiles(response string) []PreviewFile {
|
|
||||||
startMarker := "<<<PREVIEW_JSON>>>"
|
|
||||||
endMarker := "<<<END_PREVIEW>>>"
|
|
||||||
start := strings.Index(response, startMarker)
|
|
||||||
end := strings.Index(response, endMarker)
|
|
||||||
if start == -1 || end == -1 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonStr := strings.TrimSpace(response[start+len(startMarker) : end])
|
|
||||||
var files []PreviewFile
|
|
||||||
if err := json.Unmarshal([]byte(jsonStr), &files); err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return files
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseApproval(response string) (approved bool, feedback string) {
|
|
||||||
lower := strings.ToLower(strings.TrimSpace(response))
|
|
||||||
if strings.Contains(lower, "plan_approved") || strings.Contains(lower, "approved") || strings.Contains(lower, "yes") || strings.Contains(lower, "go ahead") || strings.Contains(lower, "oui") || strings.Contains(lower, "ok") {
|
|
||||||
return true, ""
|
|
||||||
}
|
|
||||||
if strings.Contains(lower, "plan_rejected:") {
|
|
||||||
parts := strings.SplitN(lower, "plan_rejected:", 2)
|
|
||||||
if len(parts) > 1 {
|
|
||||||
return false, strings.TrimSpace(parts[1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false, response
|
|
||||||
}
|
|
||||||
@@ -1,255 +0,0 @@
|
|||||||
package workflow
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNew(t *testing.T) {
|
|
||||||
wf := New()
|
|
||||||
if wf.Phase != PhaseIdle {
|
|
||||||
t.Errorf("Expected PhaseIdle, got %s", wf.Phase)
|
|
||||||
}
|
|
||||||
if wf.Plan == nil {
|
|
||||||
t.Error("Plan should not be nil")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStart(t *testing.T) {
|
|
||||||
wf := New()
|
|
||||||
wf.Start("Build a REST API")
|
|
||||||
if wf.Phase != PhaseGathering {
|
|
||||||
t.Errorf("Expected PhaseGathering, got %s", wf.Phase)
|
|
||||||
}
|
|
||||||
if wf.Plan.Goal != "Build a REST API" {
|
|
||||||
t.Errorf("Expected goal 'Build a REST API', got %s", wf.Plan.Goal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAddAnswer(t *testing.T) {
|
|
||||||
wf := New()
|
|
||||||
wf.Start("test goal")
|
|
||||||
wf.Plan.Questions = []string{"Q1?", "Q2?"}
|
|
||||||
|
|
||||||
wf.AddAnswer("A1")
|
|
||||||
if wf.Phase != PhaseGathering {
|
|
||||||
t.Errorf("Should still be gathering, got %s", wf.Phase)
|
|
||||||
}
|
|
||||||
|
|
||||||
wf.AddAnswer("A2")
|
|
||||||
if wf.Phase != PhasePlanning {
|
|
||||||
t.Errorf("Should move to planning, got %s", wf.Phase)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSetPlan(t *testing.T) {
|
|
||||||
wf := New()
|
|
||||||
planJSON := `[{"id":"1","title":"Step 1","description":"Do something","agent":"crush","status":"pending"}]`
|
|
||||||
err := wf.SetPlan(planJSON)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("SetPlan failed: %v", err)
|
|
||||||
}
|
|
||||||
if len(wf.Plan.Steps) != 1 {
|
|
||||||
t.Errorf("Expected 1 step, got %d", len(wf.Plan.Steps))
|
|
||||||
}
|
|
||||||
if wf.Phase != PhaseReviewing {
|
|
||||||
t.Errorf("Expected PhaseReviewing, got %s", wf.Phase)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestApprove(t *testing.T) {
|
|
||||||
wf := New()
|
|
||||||
wf.Start("test")
|
|
||||||
wf.Plan.Steps = []Step{{ID: "1", Title: "Step 1", Status: "pending"}}
|
|
||||||
wf.Phase = PhaseReviewing
|
|
||||||
wf.Approve()
|
|
||||||
if wf.Phase != PhaseExecuting {
|
|
||||||
t.Errorf("Expected PhaseExecuting, got %s", wf.Phase)
|
|
||||||
}
|
|
||||||
if wf.Plan.StepIndex != 0 {
|
|
||||||
t.Errorf("Expected step index 0, got %d", wf.Plan.StepIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestReject(t *testing.T) {
|
|
||||||
wf := New()
|
|
||||||
wf.Phase = PhaseReviewing
|
|
||||||
wf.Reject("too complex")
|
|
||||||
if wf.Phase != PhasePlanning {
|
|
||||||
t.Errorf("Expected PhasePlanning, got %s", wf.Phase)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAdvanceStep(t *testing.T) {
|
|
||||||
wf := New()
|
|
||||||
wf.Plan.Steps = []Step{
|
|
||||||
{ID: "1", Title: "Step 1", Status: "pending"},
|
|
||||||
{ID: "2", Title: "Step 2", Status: "pending"},
|
|
||||||
}
|
|
||||||
wf.Phase = PhaseExecuting
|
|
||||||
|
|
||||||
wf.AdvanceStep("output1")
|
|
||||||
if wf.Plan.Steps[0].Status != "done" {
|
|
||||||
t.Error("First step should be done")
|
|
||||||
}
|
|
||||||
if wf.Plan.StepIndex != 1 {
|
|
||||||
t.Errorf("Expected step index 1, got %d", wf.Plan.StepIndex)
|
|
||||||
}
|
|
||||||
if wf.Phase != PhaseExecuting {
|
|
||||||
t.Errorf("Should still be executing, got %s", wf.Phase)
|
|
||||||
}
|
|
||||||
|
|
||||||
wf.AdvanceStep("output2")
|
|
||||||
if wf.Phase != PhaseDone {
|
|
||||||
t.Errorf("Expected PhaseDone, got %s", wf.Phase)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFailStep(t *testing.T) {
|
|
||||||
wf := New()
|
|
||||||
wf.Plan.Steps = []Step{{ID: "1", Title: "Step 1"}}
|
|
||||||
wf.Phase = PhaseExecuting
|
|
||||||
|
|
||||||
wf.FailStep("something broke")
|
|
||||||
if wf.Phase != PhaseError {
|
|
||||||
t.Errorf("Expected PhaseError, got %s", wf.Phase)
|
|
||||||
}
|
|
||||||
if wf.Plan.Steps[0].Status != "error" {
|
|
||||||
t.Error("Step should have error status")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestReset(t *testing.T) {
|
|
||||||
wf := New()
|
|
||||||
wf.Start("test")
|
|
||||||
wf.Phase = PhaseExecuting
|
|
||||||
wf.Reset()
|
|
||||||
if wf.Phase != PhaseIdle {
|
|
||||||
t.Errorf("Expected PhaseIdle, got %s", wf.Phase)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCurrentStep(t *testing.T) {
|
|
||||||
wf := New()
|
|
||||||
if wf.CurrentStep() != nil {
|
|
||||||
t.Error("Should be nil with no steps")
|
|
||||||
}
|
|
||||||
|
|
||||||
wf.Plan.Steps = []Step{{ID: "1"}, {ID: "2"}}
|
|
||||||
wf.Plan.StepIndex = 0
|
|
||||||
step := wf.CurrentStep()
|
|
||||||
if step == nil || step.ID != "1" {
|
|
||||||
t.Error("Should return first step")
|
|
||||||
}
|
|
||||||
|
|
||||||
wf.Plan.StepIndex = 2
|
|
||||||
if wf.CurrentStep() != nil {
|
|
||||||
t.Error("Should be nil when past all steps")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProgress(t *testing.T) {
|
|
||||||
wf := New()
|
|
||||||
wf.Plan.Steps = []Step{
|
|
||||||
{ID: "1", Status: "done"},
|
|
||||||
{ID: "2", Status: "pending"},
|
|
||||||
{ID: "3", Status: "done"},
|
|
||||||
}
|
|
||||||
done, total := wf.Progress()
|
|
||||||
if done != 2 || total != 3 {
|
|
||||||
t.Errorf("Expected 2/3, got %d/%d", done, total)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParsePlanResponse(t *testing.T) {
|
|
||||||
resp := `Here is the plan:
|
|
||||||
[
|
|
||||||
{"id": "1", "title": "Setup", "description": "Init project", "agent": "crush"},
|
|
||||||
{"id": "2", "title": "Build", "description": "Write code", "agent": "claude"}
|
|
||||||
]`
|
|
||||||
steps, err := ParsePlanResponse(resp)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ParsePlanResponse failed: %v", err)
|
|
||||||
}
|
|
||||||
if len(steps) != 2 {
|
|
||||||
t.Errorf("Expected 2 steps, got %d", len(steps))
|
|
||||||
}
|
|
||||||
if steps[0].ID != "1" {
|
|
||||||
t.Errorf("Expected step ID 1, got %s", steps[0].ID)
|
|
||||||
}
|
|
||||||
for _, s := range steps {
|
|
||||||
if s.Status != "pending" {
|
|
||||||
t.Errorf("Steps should be pending, got %s", s.Status)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParsePlanResponseInvalid(t *testing.T) {
|
|
||||||
_, err := ParsePlanResponse("no json here")
|
|
||||||
if err == nil {
|
|
||||||
t.Error("Should fail with no JSON")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseApproval(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
input string
|
|
||||||
approved bool
|
|
||||||
}{
|
|
||||||
{"plan_approved", true},
|
|
||||||
{"approved", true},
|
|
||||||
{"yes", true},
|
|
||||||
{"ok", true},
|
|
||||||
{"oui", true},
|
|
||||||
{"go ahead", true},
|
|
||||||
{"no", false},
|
|
||||||
{"plan_rejected: too complex", false},
|
|
||||||
{"I don't like it", false},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
approved, feedback := ParseApproval(tt.input)
|
|
||||||
if approved != tt.approved {
|
|
||||||
t.Errorf("ParseApproval(%q) = %v, want %v", tt.input, approved, tt.approved)
|
|
||||||
}
|
|
||||||
if !approved && tt.input == "plan_rejected: too complex" {
|
|
||||||
if feedback != "too complex" {
|
|
||||||
t.Errorf("Expected feedback 'too complex', got %s", feedback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParsePreviewFiles(t *testing.T) {
|
|
||||||
resp := `Some text
|
|
||||||
<<<PREVIEW_JSON>>>
|
|
||||||
[{"filename":"test.html","content":"<h1>Hello</h1>","type":"html"}]
|
|
||||||
<<<END_PREVIEW>>>`
|
|
||||||
files := ParsePreviewFiles(resp)
|
|
||||||
if len(files) != 1 {
|
|
||||||
t.Fatalf("Expected 1 file, got %d", len(files))
|
|
||||||
}
|
|
||||||
if files[0].Filename != "test.html" {
|
|
||||||
t.Errorf("Expected test.html, got %s", files[0].Filename)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParsePreviewFilesNone(t *testing.T) {
|
|
||||||
files := ParsePreviewFiles("no preview here")
|
|
||||||
if files != nil {
|
|
||||||
t.Error("Should return nil")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildSystemPrompt(t *testing.T) {
|
|
||||||
prompt := BuildSystemPrompt(PhaseIdle, &Plan{})
|
|
||||||
if prompt == "" {
|
|
||||||
t.Error("Prompt should not be empty")
|
|
||||||
}
|
|
||||||
if len(prompt) < 100 {
|
|
||||||
t.Error("Prompt seems too short")
|
|
||||||
}
|
|
||||||
|
|
||||||
prompt = BuildSystemPrompt(PhaseGathering, &Plan{Goal: "test"})
|
|
||||||
if prompt == "" {
|
|
||||||
t.Error("Gathering prompt should not be empty")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user