diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 0eb31e3b6..a08d0287f 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -10,6 +10,7 @@ + diff --git a/android/app/src/main/java/io/picoclaw/android/assistant/ToolRequestHandler.kt b/android/app/src/main/java/io/picoclaw/android/assistant/ToolRequestHandler.kt index 2bfc7e432..21bb77daf 100644 --- a/android/app/src/main/java/io/picoclaw/android/assistant/ToolRequestHandler.kt +++ b/android/app/src/main/java/io/picoclaw/android/assistant/ToolRequestHandler.kt @@ -25,7 +25,7 @@ class ToolRequestHandler( suspend fun handle(request: ToolRequest): ToolResponse { return try { when (request.action) { - "list_apps" -> handleListApps(request) + "search_apps" -> handleSearchApps(request) "app_info" -> handleAppInfo(request) "launch_app" -> handleLaunchApp(request) "current_activity" -> handleCurrentActivity(request) @@ -63,21 +63,38 @@ class ToolRequestHandler( return null } - private fun handleListApps(request: ToolRequest): ToolResponse { + private fun handleSearchApps(request: ToolRequest): ToolResponse { + val query = request.params?.get("query")?.jsonPrimitive?.contentOrNull + ?: return ToolResponse(request.requestId, false, error = "query required") + val pm = context.packageManager - val apps = pm.getInstalledApplications(PackageManager.GET_META_DATA) - .filter { pm.getLaunchIntentForPackage(it.packageName) != null } + val q = query.lowercase() + val matches = pm.getInstalledApplications(PackageManager.GET_META_DATA) + .filter { app -> + val label = pm.getApplicationLabel(app).toString().lowercase() + label.contains(q) || app.packageName.lowercase().contains(q) + } .map { app -> val label = pm.getApplicationLabel(app).toString() - "${label} (${app.packageName})" + val launchable = pm.getLaunchIntentForPackage(app.packageName) != null + val isSystem = app.flags and ApplicationInfo.FLAG_SYSTEM != 0 + buildString { + append("$label (${app.packageName})") + if (launchable) append(" [launchable]") + if (isSystem) append(" [system]") + } } .sorted() - return ToolResponse( - requestId = request.requestId, - success = true, - result = "Installed apps (${apps.size}):\n${apps.joinToString("\n")}" - ) + return if (matches.isEmpty()) { + ToolResponse(request.requestId, true, result = "No apps found matching \"$query\"") + } else { + ToolResponse( + requestId = request.requestId, + success = true, + result = "Found ${matches.size} app(s) matching \"$query\":\n${matches.joinToString("\n")}" + ) + } } private fun handleAppInfo(request: ToolRequest): ToolResponse { diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 36bb381ff..62e46a40c 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -107,7 +107,7 @@ func createToolRegistry(workspace string, restrict bool, cfg *config.Config, msg registry.Register(tools.NewSPITool()) } - // Android device control tool (assistant mode only) + // Android device control tool if cfg.Tools.Android.Enabled { androidTool := tools.NewAndroidTool() androidTool.SetSendCallback(func(channel, chatID, content, msgType string) error { @@ -992,11 +992,8 @@ func (al *AgentLoop) updateToolContexts(channel, chatID string, metadata map[str } } if tool, ok := al.tools.Get("android"); ok { - if at, ok := tool.(*tools.AndroidTool); ok { - at.SetContext(channel, chatID) - if metadata != nil { - at.SetClientType(metadata["client_type"]) - } + if ct, ok := tool.(tools.ContextualTool); ok { + ct.SetContext(channel, chatID) } } } diff --git a/pkg/config/config.go b/pkg/config/config.go index ee84b0f0c..6565949cf 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -380,7 +380,7 @@ func DefaultConfig() *Config { Enabled: false, }, Android: AndroidToolsConfig{ - Enabled: false, + Enabled: true, }, Web: WebToolsConfig{ Brave: BraveConfig{ diff --git a/pkg/tools/android.go b/pkg/tools/android.go index dc5bb3964..0f5e2127c 100644 --- a/pkg/tools/android.go +++ b/pkg/tools/android.go @@ -32,7 +32,6 @@ type AndroidTool struct { sendCallback SendCallbackWithType channel string chatID string - clientType string } func NewAndroidTool() *AndroidTool { @@ -43,7 +42,7 @@ func (t *AndroidTool) Name() string { return "android" } func (t *AndroidTool) Description() string { return `Control the Android device. Available actions: -- list_apps: List installed apps +- search_apps: Search installed apps by name or package name (requires query) - app_info: Get app details (requires package_name) - launch_app: Launch an app (requires package_name) - current_activity: Get the currently active app/window @@ -53,8 +52,7 @@ func (t *AndroidTool) Description() string { - keyevent: Press a key (requires key: back/home/recents) - broadcast: Send a broadcast intent (requires intent_action; optional intent_extras) - intent: Start an activity via intent (requires intent_action; optional intent_data, intent_package, intent_type, intent_extras) - -Only available in assistant mode.` +` } func (t *AndroidTool) Parameters() map[string]interface{} { @@ -64,12 +62,16 @@ func (t *AndroidTool) Parameters() map[string]interface{} { "action": map[string]interface{}{ "type": "string", "enum": []string{ - "list_apps", "app_info", "launch_app", "current_activity", + "search_apps", "app_info", "launch_app", "current_activity", "tap", "swipe", "text", "keyevent", "broadcast", "intent", }, "description": "The device action to perform", }, + "query": map[string]interface{}{ + "type": "string", + "description": "Search query for app name or package name (for search_apps)", + }, "package_name": map[string]interface{}{ "type": "string", "description": "Android package name (for app_info, launch_app)", @@ -133,21 +135,11 @@ func (t *AndroidTool) SetContext(channel, chatID string) { t.chatID = chatID } -func (t *AndroidTool) SetClientType(clientType string) { - t.clientType = clientType -} - func (t *AndroidTool) SetSendCallback(cb SendCallbackWithType) { t.sendCallback = cb } func (t *AndroidTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult { - if t.clientType == "" { - return ErrorResult("android tool: not available (no assistant session)") - } - if t.clientType != "assistant" { - return ErrorResult("android tool is only available in assistant mode") - } if t.sendCallback == nil { return ErrorResult("android tool: send callback not configured") } @@ -172,8 +164,12 @@ func (t *AndroidTool) validateAndBuildParams(action string, args map[string]inte params := make(map[string]interface{}) switch action { - case "list_apps": - // No params needed + case "search_apps": + query, _ := args["query"].(string) + if query == "" { + return nil, fmt.Errorf("search_apps requires query") + } + params["query"] = query case "app_info", "launch_app": pkg, _ := args["package_name"].(string)