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)