/ internal / tools / wait.go
wait.go
  1  package tools
  2  
  3  import (
  4  	"context"
  5  	"encoding/json"
  6  	"fmt"
  7  	"runtime"
  8  
  9  	"github.com/Kocoro-lab/ShanClaw/internal/agent"
 10  )
 11  
 12  // WaitTool wraps the ax_server wait_for method.
 13  type WaitTool struct {
 14  	client *AXClient
 15  }
 16  
 17  type waitArgs struct {
 18  	Condition string  `json:"condition"`
 19  	Value     string  `json:"value,omitempty"`
 20  	Query     string  `json:"query,omitempty"`
 21  	Role      string  `json:"role,omitempty"`
 22  	App       string  `json:"app,omitempty"`
 23  	Timeout   float64 `json:"timeout,omitempty"`
 24  	Interval  float64 `json:"interval,omitempty"`
 25  }
 26  
 27  func (t *WaitTool) Info() agent.ToolInfo {
 28  	return agent.ToolInfo{
 29  		Name:        "wait_for",
 30  		Description: "Wait for a UI condition instead of fixed delays. Use after navigation, app launch, or actions that trigger async changes. Conditions: elementExists, elementGone, titleContains, urlContains, titleChanged, urlChanged. Always use this instead of 'sleep' or 'bash sleep'.",
 31  		Parameters: map[string]any{
 32  			"type": "object",
 33  			"properties": map[string]any{
 34  				"condition": map[string]any{"type": "string", "description": "Condition to wait for: elementExists, elementGone, titleContains, urlContains, titleChanged, urlChanged"},
 35  				"value":     map[string]any{"type": "string", "description": "Substring to match (for titleContains, urlContains)"},
 36  				"query":     map[string]any{"type": "string", "description": "Text to search for (for elementExists, elementGone)"},
 37  				"role":      map[string]any{"type": "string", "description": "AX role filter (for elementExists, elementGone, e.g. AXButton)"},
 38  				"app":       map[string]any{"type": "string", "description": "Target app name (defaults to frontmost app)"},
 39  				"timeout":   map[string]any{"type": "number", "description": "Max seconds to wait (default: 10)"},
 40  				"interval":  map[string]any{"type": "number", "description": "Poll interval in seconds (default: 0.5)"},
 41  			},
 42  			"required": []string{"condition"},
 43  		},
 44  		Required: []string{"condition"},
 45  	}
 46  }
 47  
 48  func (t *WaitTool) RequiresApproval() bool { return false }
 49  
 50  func (t *WaitTool) IsReadOnlyCall(string) bool { return true }
 51  
 52  func (t *WaitTool) Run(ctx context.Context, argsJSON string) (agent.ToolResult, error) {
 53  	if runtime.GOOS != "darwin" || t.client == nil {
 54  		return agent.ToolResult{Content: "wait_for tool is only available on macOS", IsError: true}, nil
 55  	}
 56  
 57  	var args waitArgs
 58  	if err := json.Unmarshal([]byte(argsJSON), &args); err != nil {
 59  		return agent.ToolResult{Content: fmt.Sprintf("invalid arguments: %v", err), IsError: true}, nil
 60  	}
 61  
 62  	if args.Condition == "" {
 63  		return agent.ToolResult{Content: "missing required parameter: condition", IsError: true}, nil
 64  	}
 65  
 66  	// Resolve PID from app name
 67  	var pid int
 68  	if args.App != "" {
 69  		if !validAppNamePattern.MatchString(args.App) {
 70  			return agent.ToolResult{
 71  				Content: fmt.Sprintf("invalid app name %q", args.App),
 72  				IsError: true,
 73  			}, nil
 74  		}
 75  		result, err := t.client.Call(ctx, "resolve_pid", map[string]any{"app_name": args.App})
 76  		if err != nil {
 77  			return agent.ToolResult{
 78  				Content: fmt.Sprintf("app %q not found or not running", args.App),
 79  				IsError: true,
 80  			}, nil
 81  		}
 82  		var pidResult struct {
 83  			PID int `json:"pid"`
 84  		}
 85  		if err := json.Unmarshal(result, &pidResult); err != nil {
 86  			return agent.ToolResult{
 87  				Content: fmt.Sprintf("could not parse PID for %q", args.App),
 88  				IsError: true,
 89  			}, nil
 90  		}
 91  		pid = pidResult.PID
 92  	}
 93  
 94  	params := map[string]any{
 95  		"condition": args.Condition,
 96  	}
 97  	if pid > 0 {
 98  		params["pid"] = pid
 99  	}
100  	if args.Value != "" {
101  		params["value"] = args.Value
102  	}
103  	if args.Query != "" {
104  		params["query"] = args.Query
105  	}
106  	if args.Role != "" {
107  		params["role"] = args.Role
108  	}
109  	if args.Timeout > 0 {
110  		params["timeout"] = args.Timeout
111  	}
112  	if args.Interval > 0 {
113  		params["interval"] = args.Interval
114  	}
115  
116  	result, err := t.client.Call(ctx, "wait_for", params)
117  	if err != nil {
118  		return agent.ToolResult{Content: fmt.Sprintf("wait_for: %v", err), IsError: true}, nil
119  	}
120  
121  	var actionResult struct {
122  		Result string `json:"result"`
123  	}
124  	json.Unmarshal(result, &actionResult)
125  	return agent.ToolResult{Content: actionResult.Result}, nil
126  }