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 }