Skip to content

Commit c9b931e

Browse files
authored
2026.03.18 (#109)
1 parent 21effe4 commit c9b931e

File tree

149 files changed

+3966
-1282
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

149 files changed

+3966
-1282
lines changed

docs/CLI.md

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,37 @@ trailblaze [OPTIONS] [COMMAND]
2222

2323
| Command | Description |
2424
|---------|-------------|
25+
| `desktop` | Launch the Trailblaze desktop application |
2526
| `run` | Run a .trail.yaml file or directory of trail files on a connected device |
2627
| `mcp` | Start the MCP server |
27-
| `list-devices` | List all connected devices |
28+
| `devices` | List all connected devices |
2829
| `config` | View and modify Trailblaze configuration |
2930
| `status` | Check if the Trailblaze daemon is running |
3031
| `stop` | Stop the Trailblaze daemon |
3132
| `help` | When no COMMAND is given, the usage help for the main command is displayed. |
3233

3334
---
3435

36+
### `trailblaze desktop`
37+
38+
Launch the Trailblaze desktop application
39+
40+
**Synopsis:**
41+
42+
```
43+
trailblaze desktop [OPTIONS]
44+
```
45+
46+
**Options:**
47+
48+
| Option | Description | Default |
49+
|--------|-------------|---------|
50+
| `--headless` | Start in headless mode (MCP server only, no GUI) | - |
51+
| `-h`, `--help` | Show this help message and exit. | - |
52+
| `-V`, `--version` | Print version information and exit. | - |
53+
54+
---
55+
3556
### `trailblaze run`
3657

3758
Run a .trail.yaml file or directory of trail files on a connected device
@@ -54,7 +75,7 @@ trailblaze run [OPTIONS] <<trailFile>>
5475
|--------|-------------|---------|
5576
| `--headless` | Run without GUI (MCP server mode) | - |
5677
| `-d`, `--device` | Device ID to run on (e.g., 'emulator-5554'). If not specified, uses first available device. | - |
57-
| `-a`, `--agent` | Agent: TRAILBLAZE_RUNNER, TWO_TIER_AGENT, MULTI_AGENT_V3. Default: TRAILBLAZE_RUNNER | - |
78+
| `-a`, `--agent` | Agent: TRAILBLAZE_RUNNER, MULTI_AGENT_V3. Default: TRAILBLAZE_RUNNER | - |
5879
| `--use-recorded-steps` | Use recorded tool sequences instead of LLM inference | - |
5980
| `--set-of-mark` | Enable Set of Mark mode (default: true) | - |
6081
| `-v`, `--verbose` | Enable verbose output | - |
@@ -121,20 +142,19 @@ trailblaze mcp install [OPTIONS] [<<target>>]
121142

122143
| Option | Description | Default |
123144
|--------|-------------|---------|
124-
| `--port` | MCP endpoint port (default: 52525) | - |
125145
| `-h`, `--help` | Show this help message and exit. | - |
126146
| `-V`, `--version` | Print version information and exit. | - |
127147

128148
---
129149

130-
### `trailblaze list-devices`
150+
### `trailblaze devices`
131151

132152
List all connected devices
133153

134154
**Synopsis:**
135155

136156
```
137-
trailblaze list-devices [OPTIONS]
157+
trailblaze devices [OPTIONS]
138158
```
139159

140160
**Options:**
@@ -180,7 +200,7 @@ trailblaze config drivers
180200
| `llm` | LLM provider and model (shorthand: provider/model) | provider/model (e.g., openai/gpt-4-1, anthropic/claude-sonnet-4-20250514) |
181201
| `llm-provider` | LLM provider | openai, anthropic, google, ollama, openrouter, etc. |
182202
| `llm-model` | LLM model ID | e.g., gpt-4-1, claude-sonnet-4-20250514, gemini-3-flash |
183-
| `agent` | Agent implementation | TRAILBLAZE_RUNNER, TWO_TIER_AGENT, MULTI_AGENT_V3 |
203+
| `agent` | Agent implementation | TRAILBLAZE_RUNNER, MULTI_AGENT_V3 |
184204
| `android-driver` | Android driver type | HOST, ONDEVICE, ACCESSIBILITY |
185205
| `ios-driver` | iOS driver type | HOST |
186206
| `set-of-mark` | Enable/disable Set of Mark mode | true, false |
@@ -194,7 +214,7 @@ trailblaze config llm # Show current LLM provider
194214
trailblaze config llm anthropic/claude-sonnet-4-6 # Set both provider + model
195215
trailblaze config llm-provider openai # Set provider only
196216
trailblaze config llm-model gpt-4-1 # Set model only
197-
trailblaze config agent TWO_TIER_AGENT # Set agent implementation
217+
trailblaze config agent MULTI_AGENT_V3 # Set agent implementation
198218
trailblaze config set-of-mark false # Disable Set of Mark
199219
trailblaze config models # List available LLM models
200220
trailblaze config agents # List agent implementations

docs/generator/src/main/java/xyz/block/trailblaze/docs/CliDocsGenerator.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ class CliDocsGenerator(
178178
appendLine("trailblaze config llm anthropic/claude-sonnet-4-6 # Set both provider + model")
179179
appendLine("trailblaze config llm-provider openai # Set provider only")
180180
appendLine("trailblaze config llm-model gpt-4-1 # Set model only")
181-
appendLine("trailblaze config agent TWO_TIER_AGENT # Set agent implementation")
181+
appendLine("trailblaze config agent MULTI_AGENT_V3 # Set agent implementation")
182182
appendLine("trailblaze config set-of-mark false # Disable Set of Mark")
183183
appendLine("trailblaze config models # List available LLM models")
184184
appendLine("trailblaze config agents # List agent implementations")
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
plugins {
2+
alias(libs.plugins.android.library)
3+
alias(libs.plugins.kotlin.android)
4+
id("trailblaze.spotless")
5+
}
6+
7+
android {
8+
namespace = "xyz.block.trailblaze.examples.sampleapp.uitests"
9+
compileSdk = 35
10+
defaultConfig {
11+
minSdk = 28
12+
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
13+
}
14+
15+
sourceSets {
16+
getByName("androidTest") {
17+
// Bundle sample-app trails as assets in the test APK so runFromAsset() can read them.
18+
assets.srcDirs("../android-sample-app/trails")
19+
java.srcDirs("src/androidTest/java", "src/androidTest/generated")
20+
}
21+
}
22+
23+
packaging {
24+
exclude("META-INF/INDEX.LIST")
25+
exclude("META-INF/AL2.0")
26+
exclude("META-INF/LICENSE.md")
27+
exclude("META-INF/LICENSE-notice.md")
28+
exclude("META-INF/LGPL2.1")
29+
exclude("META-INF/io.netty.versions.properties")
30+
}
31+
32+
compileOptions {
33+
sourceCompatibility = JavaVersion.VERSION_17
34+
targetCompatibility = JavaVersion.VERSION_17
35+
}
36+
kotlin { jvmToolchain(17) }
37+
lint { abortOnError = false }
38+
testOptions { animationsDisabled = true }
39+
}
40+
41+
dependencies {
42+
androidTestImplementation(project(":trailblaze-common"))
43+
androidTestImplementation(project(":trailblaze-android"))
44+
androidTestImplementation(libs.junit)
45+
androidTestImplementation(libs.koog.prompt.executor.ollama)
46+
androidTestImplementation(libs.koog.prompt.executor.openai)
47+
androidTestImplementation(libs.koog.prompt.executor.openrouter)
48+
androidTestImplementation(libs.koog.prompt.executor.clients)
49+
androidTestImplementation(libs.koog.prompt.llm)
50+
androidTestImplementation(libs.ktor.client.core)
51+
androidTestImplementation(libs.kotlinx.datetime)
52+
androidTestRuntimeOnly(libs.androidx.test.runner)
53+
androidTestRuntimeOnly(libs.coroutines.android)
54+
androidTestImplementation(libs.maestro.orchestra.models) { isTransitive = false }
55+
}
56+
57+
// ---------------------------------------------------------------------------
58+
// generateSampleAppTests
59+
// Scans ../android-sample-app/trails/android-ondevice-instrumentation/**/*.trail.yaml
60+
// and writes a JUnit test class so the sample-app trails can run on Test Farm.
61+
// Usage: ./gradlew :examples:android-sample-app-uitests:generateSampleAppTests
62+
// ---------------------------------------------------------------------------
63+
tasks.register("generateSampleAppTests") {
64+
description = "Generate JUnit instrumentation tests from trail YAML files for Test Farm"
65+
group = "trailblaze"
66+
67+
val trailsDir = file("../android-sample-app/trails/android-ondevice-instrumentation")
68+
val outputFile =
69+
file(
70+
"src/androidTest/generated/xyz/block/trailblaze/examples/sampleapp/generated/GeneratedSampleAppTests.kt"
71+
)
72+
73+
inputs.dir(trailsDir)
74+
outputs.file(outputFile)
75+
76+
doLast {
77+
outputFile.parentFile.mkdirs()
78+
79+
val testMethods =
80+
fileTree(trailsDir)
81+
.filter { it.name.endsWith(".trail.yaml") }
82+
.map { trailFile ->
83+
val relPath = trailFile.relativeTo(trailsDir).path
84+
val testDirName = relPath.split("/").let { it[it.size - 2] }
85+
val methodName =
86+
testDirName
87+
.split("-")
88+
.mapIndexed { i, s -> if (i == 0) s else s.replaceFirstChar { c -> c.uppercase() } }
89+
.joinToString("")
90+
val assetPath = "android-ondevice-instrumentation/$relPath"
91+
Pair(methodName, assetPath)
92+
}
93+
.sortedBy { it.first }
94+
95+
outputFile.writeText(
96+
buildString {
97+
appendLine("// AUTO-GENERATED — do not edit manually.")
98+
appendLine(
99+
"// Re-generate: ./gradlew :examples:android-sample-app-uitests:generateSampleAppTests"
100+
)
101+
appendLine()
102+
appendLine("package xyz.block.trailblaze.examples.sampleapp.generated")
103+
appendLine()
104+
appendLine("import org.junit.Rule")
105+
appendLine("import org.junit.Test")
106+
appendLine("import xyz.block.trailblaze.examples.sampleapp.SampleAppTrailblazeRule")
107+
appendLine()
108+
appendLine("class GeneratedSampleAppTests {")
109+
appendLine()
110+
appendLine(" @get:Rule val rule = SampleAppTrailblazeRule()")
111+
appendLine()
112+
testMethods.forEach { (method, path) ->
113+
appendLine(" @Test fun $method() = rule.runFromAsset(\"$path\")")
114+
}
115+
appendLine("}")
116+
}
117+
)
118+
119+
println("Generated ${testMethods.size} tests → ${outputFile.relativeTo(projectDir)}")
120+
}
121+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// AUTO-GENERATED — do not edit manually.
2+
// Re-generate: ./gradlew :examples:android-sample-app-uitests:generateSampleAppTests
3+
4+
package xyz.block.trailblaze.examples.sampleapp.generated
5+
6+
import org.junit.Rule
7+
import org.junit.Test
8+
import xyz.block.trailblaze.examples.sampleapp.SampleAppTrailblazeRule
9+
10+
class GeneratedSampleAppTests {
11+
12+
@get:Rule val rule = SampleAppTrailblazeRule()
13+
14+
@Test
15+
fun conditionalItem() =
16+
rule.runFromAsset(
17+
"android-ondevice-instrumentation/catalog/conditional-item/android-phone.trail.yaml"
18+
)
19+
20+
@Test
21+
fun multipleItems() =
22+
rule.runFromAsset(
23+
"android-ondevice-instrumentation/catalog/multiple-items/android-phone.trail.yaml"
24+
)
25+
26+
@Test
27+
fun overlayTap() =
28+
rule.runFromAsset(
29+
"android-ondevice-instrumentation/catalog/overlay-tap/android-phone.trail.yaml"
30+
)
31+
32+
@Test
33+
fun scrollAndNavigate() =
34+
rule.runFromAsset(
35+
"android-ondevice-instrumentation/lists/scroll-and-navigate/android-phone.trail.yaml"
36+
)
37+
38+
@Test
39+
fun simpleTap() =
40+
rule.runFromAsset("android-ondevice-instrumentation/taps/simple-tap/android-phone.trail.yaml")
41+
42+
@Test
43+
fun swipeDirections() =
44+
rule.runFromAsset(
45+
"android-ondevice-instrumentation/swipe/swipe-directions/android-phone.trail.yaml"
46+
)
47+
48+
@Test
49+
fun systemInteractions() =
50+
rule.runFromAsset(
51+
"android-ondevice-instrumentation/settings/system-interactions/android-phone.trail.yaml"
52+
)
53+
54+
@Test
55+
fun tapInteractions() =
56+
rule.runFromAsset(
57+
"android-ondevice-instrumentation/taps/tap-interactions/android-phone.trail.yaml"
58+
)
59+
60+
@Test
61+
fun textInput() =
62+
rule.runFromAsset("android-ondevice-instrumentation/forms/text-input/android-phone.trail.yaml")
63+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package xyz.block.trailblaze.examples.sampleapp
2+
3+
import ai.koog.prompt.executor.clients.openai.OpenAIClientSettings
4+
import ai.koog.prompt.executor.clients.openai.OpenAILLMClient
5+
import ai.koog.prompt.executor.clients.openrouter.OpenRouterLLMClient
6+
import ai.koog.prompt.executor.ollama.client.OllamaClient
7+
import ai.koog.prompt.llm.LLMProvider
8+
import xyz.block.trailblaze.android.AndroidTrailblazeRule
9+
import xyz.block.trailblaze.android.InstrumentationArgUtil
10+
import xyz.block.trailblaze.android.openai.OpenAiInstrumentationArgUtil
11+
import xyz.block.trailblaze.http.DefaultDynamicLlmClient
12+
import xyz.block.trailblaze.http.TrailblazeHttpClientFactory
13+
import xyz.block.trailblaze.llm.TrailblazeLlmModel
14+
import xyz.block.trailblaze.llm.providers.OpenAITrailblazeLlmModelList
15+
import xyz.block.trailblaze.llm.providers.OpenRouterTrailblazeLlmModelList
16+
17+
class SampleAppTrailblazeRule(
18+
trailblazeLlmModel: TrailblazeLlmModel =
19+
InstrumentationArgUtil.resolveTrailblazeLlmModel(
20+
"OPENROUTER_API_KEY" to OpenRouterTrailblazeLlmModelList.GPT_OSS_120B_FREE,
21+
"OPENAI_API_KEY" to OpenAITrailblazeLlmModelList.OPENAI_GPT_4_1,
22+
)
23+
) :
24+
AndroidTrailblazeRule(
25+
llmClient =
26+
DefaultDynamicLlmClient(
27+
trailblazeLlmModel = trailblazeLlmModel,
28+
llmClients =
29+
buildMap {
30+
put(LLMProvider.Ollama, OllamaClient(baseClient = httpClient))
31+
put(
32+
LLMProvider.OpenAI,
33+
OpenAILLMClient(
34+
baseClient = httpClient,
35+
apiKey =
36+
InstrumentationArgUtil.getInstrumentationArg("OPENAI_API_KEY")
37+
?: "OPENAI_API_KEY NOT SET",
38+
settings =
39+
OpenAIClientSettings(
40+
baseUrl = OpenAiInstrumentationArgUtil.getBaseUrlFromInstrumentationArg()
41+
),
42+
),
43+
)
44+
put(
45+
LLMProvider.OpenRouter,
46+
OpenRouterLLMClient(
47+
baseClient = httpClient,
48+
apiKey =
49+
InstrumentationArgUtil.getInstrumentationArg("OPENROUTER_API_KEY")
50+
?: "OPENROUTER_API_KEY NOT SET",
51+
),
52+
)
53+
},
54+
)
55+
.createLlmClient(),
56+
trailblazeLlmModel = trailblazeLlmModel,
57+
) {
58+
companion object {
59+
private val httpClient =
60+
TrailblazeHttpClientFactory.createInsecureTrustAllCertsHttpClient(
61+
timeoutInSeconds = 120,
62+
reverseProxyUrl = InstrumentationArgUtil.reverseProxyEndpoint(),
63+
)
64+
}
65+
}

examples/android-sample-app/build.gradle.kts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,4 @@ dependencies {
4040
implementation("androidx.fragment:fragment-ktx:1.8.5")
4141

4242
debugImplementation("androidx.compose.ui:ui-tooling")
43-
4443
}

examples/android-sample-app/src/main/java/xyz/block/trailblaze/examples/sampleapp/ui/navigation/SampleAppNavigation.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.padding
44
import androidx.compose.material.icons.Icons
55
import androidx.compose.material.icons.automirrored.filled.List
66
import androidx.compose.material.icons.filled.Settings
7+
import androidx.compose.material.icons.filled.ShoppingCart
78
import androidx.compose.material.icons.filled.SwipeLeft
89
import androidx.compose.material.icons.filled.TextFields
910
import androidx.compose.material.icons.filled.TouchApp
@@ -22,6 +23,7 @@ import androidx.navigation.compose.NavHost
2223
import androidx.navigation.compose.composable
2324
import androidx.navigation.compose.currentBackStackEntryAsState
2425
import androidx.navigation.compose.rememberNavController
26+
import xyz.block.trailblaze.examples.sampleapp.ui.screens.catalog.CatalogScreen
2527
import xyz.block.trailblaze.examples.sampleapp.ui.screens.forms.FormsScreen
2628
import xyz.block.trailblaze.examples.sampleapp.ui.screens.lists.ListDetailScreen
2729
import xyz.block.trailblaze.examples.sampleapp.ui.screens.lists.ListsScreen
@@ -34,6 +36,7 @@ enum class Tab(val route: String, val label: String, val icon: ImageVector) {
3436
FORMS("forms", "Forms", Icons.Default.TextFields),
3537
LISTS("lists", "Lists", Icons.AutoMirrored.Filled.List),
3638
SWIPE("swipe", "Swipe", Icons.Default.SwipeLeft),
39+
CATALOG("catalog", "Catalog", Icons.Default.ShoppingCart),
3740
SETTINGS("settings", "Settings", Icons.Default.Settings),
3841
}
3942

@@ -77,6 +80,7 @@ fun SampleAppNavigation() {
7780
ListDetailScreen(index = index, onBack = { navController.popBackStack() })
7881
}
7982
composable(Tab.SWIPE.route) { SwipeScreen() }
83+
composable(Tab.CATALOG.route) { CatalogScreen() }
8084
composable(Tab.SETTINGS.route) { SettingsScreen() }
8185
}
8286
}

0 commit comments

Comments
 (0)