Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions .github/workflows/cleanup-stale-prs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

name: Cleanup Stale Draft PRs

on:
schedule:
- cron: '0 2 * * *' # Daily at 02:00 UTC
workflow_dispatch:
inputs:
older_than_days:
description: 'Close draft PRs older than N days'
required: false
default: '3'
type: string
dry_run:
description: 'Dry run (simulate without closing PRs or deleting branches)'
required: false
default: false
type: boolean

permissions:
pull-requests: write
contents: write

jobs:
cleanup:
name: Cleanup Draft PRs
# zizmor: ignore[unpinned-images]
runs-on: ubuntu-latest
steps:
- name: Close stale draft PRs and delete branches
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
INPUT_DAYS: ${{ inputs.older_than_days }}
INPUT_DRY_RUN: ${{ inputs.dry_run }}
run: |
DAYS="${INPUT_DAYS:-3}"
DRY_RUN="${INPUT_DRY_RUN:-false}"

echo "Searching for open draft PRs with head branch matching 'test_*' older than $DAYS day(s)..."

CUTOFF_EPOCH=$(date -d "$DAYS days ago" +%s)
echo "Cutoff timestamp: $CUTOFF_EPOCH ($(date -d "@$CUTOFF_EPOCH" --utc --iso-8601=seconds))"

PRS_JSON=$(gh pr list --repo "$GH_REPO" --state open --draft --json number,headRefName,updatedAt)

echo "$PRS_JSON" | jq -c '.[]' | while read -r pr; do
PR_NUMBER=$(echo "$pr" | jq -r '.number')
HEAD_REF=$(echo "$pr" | jq -r '.headRefName')
UPDATED_AT=$(echo "$pr" | jq -r '.updatedAt')

# Only target Copybara presubmit branches (prefix test_)
if [[ ! "$HEAD_REF" =~ ^test_ ]]; then
echo "Skipping PR #$PR_NUMBER (head branch '$HEAD_REF' does not match 'test_*')"
continue
fi

PR_EPOCH=$(date -d "$UPDATED_AT" +%s)
if [ "$PR_EPOCH" -lt "$CUTOFF_EPOCH" ]; then
echo "PR #$PR_NUMBER ($HEAD_REF, updated at $UPDATED_AT) is older than $DAYS day(s)."
if [ "$DRY_RUN" = "true" ]; then
echo "[DRY RUN] Would close PR #$PR_NUMBER and delete branch '$HEAD_REF'"
else
echo "Closing PR #$PR_NUMBER and deleting branch '$HEAD_REF'..."
gh pr close "$PR_NUMBER" --repo "$GH_REPO" --comment "Automatically closing stale presubmit draft PR and cleaning up branch." --delete-branch || \
gh pr close "$PR_NUMBER" --repo "$GH_REPO" --comment "Automatically closing stale presubmit draft PR."
fi
else
echo "Keeping PR #$PR_NUMBER ($HEAD_REF, updated at $UPDATED_AT) - active within $DAYS day(s)."
fi
done
3 changes: 3 additions & 0 deletions .github/workflows/python-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ on:

jobs:
build:
name: Python CI / build
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/web-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ on:

jobs:
build:
name: Web CI / build
runs-on: ubuntu-latest
permissions:
contents: read
Expand Down
18 changes: 17 additions & 1 deletion .github/workflows/zizmor.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

name: Zizmor

on:
Expand All @@ -23,4 +37,6 @@ jobs:
persist-credentials: false

- name: Run zizmor
uses: zizmorcore/zizmor-action@195d10ad90f31d8cd6ea1efd6ecc12969ddbe73f # v0.5.1
uses: zizmorcore/zizmor-action@195d10ad90f31d8cd6ea1efd6ecc12969ddbe73f # v0.5.1
with:
args: --ignore insufficient-cooldown
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,6 @@ Agentic UI Toolkit requires an API Key to use Google Maps Platform products. To

Your API Key must have the following APIs enabled in the [Google Cloud Console](https://console.cloud.google.com/apis/credentials):

* Geocoding API
* Maps JavaScript API
* Places UI Kit
* Routes API
Expand Down
21 changes: 17 additions & 4 deletions client/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ android {

buildFeatures {
buildConfig true
compose true
}

signingConfigs {
Expand Down Expand Up @@ -74,17 +75,29 @@ kotlin {
}

dependencies {
def composeBom = platform('androidx.compose:compose-bom:2024.02.00')
implementation composeBom
androidTestImplementation composeBom
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.ui:ui-graphics'
implementation 'androidx.compose.ui:ui-tooling-preview'
implementation 'androidx.compose.material3:material3'
implementation 'androidx.activity:activity-compose:1.8.2'
debugImplementation 'androidx.compose.ui:ui-tooling'
debugImplementation 'androidx.compose.ui:ui-test-manifest'
def lifecycleVersion = '2.8.7'
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-runtime-compose:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"

implementation 'androidx.tracing:tracing-ktx:1.2.0'
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.11.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.recyclerview:recyclerview:1.3.2'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7'
implementation 'com.squareup.okhttp3:okhttp:5.3.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
implementation 'com.google.android.libraries.mapsplatform.a2ui:GoogleMapsA2UI:0.1.0'
}
2 changes: 0 additions & 2 deletions client/android/app/src/androidTest/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.maui.test">



<instrumentation
android:name="androidx.test.runner.AndroidJUnitRunner"
android:targetPackage="com.example.maui"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,12 @@

package com.example.maui

import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
import androidx.test.espresso.action.ViewActions.replaceText
import androidx.test.espresso.action.ViewActions.swipeUp
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onAllNodesWithTag
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollToIndex
import androidx.compose.ui.test.performTextReplacement
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Rule
import org.junit.Test
Expand All @@ -33,104 +30,107 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class MainActivityTest {

@get:Rule val activityRule = ActivityScenarioRule(MainActivity::class.java)
@get:Rule val composeTestRule = createAndroidComposeRule<MainActivity>()

@Test
fun testSeattleCoffeeShopsCannedResponse() {
onView(withId(R.id.editTextMessage))
.perform(
replaceText("Show me 5 coffee shops near South Lake Union in Seattle"),
closeSoftKeyboard(),
)
onView(withId(R.id.buttonSend)).perform(click())
Thread.sleep(7000)
composeTestRule
.onNodeWithTag("editTextMessage")
.performTextReplacement("Show me 5 coffee shops near South Lake Union in Seattle")
composeTestRule.onNodeWithTag("buttonSend").performClick()
composeTestRule.waitUntil(15_000) {
composeTestRule.onAllNodesWithTag("gmpA2UIView").fetchSemanticsNodes().isNotEmpty()
}

// Verify that the A2UIView container (which renders the mock JSON) is displayed
onView(withId(R.id.gmpA2UIView)).check(matches(isDisplayed()))
// Verify that the A2UIView container (which renders the mock JSON) exists
composeTestRule.onNodeWithTag("gmpA2UIView").assertExists()

// Scroll the recycler view to see the full content
onView(withId(R.id.recyclerView)).perform(swipeUp())
// Scroll the message list to see the full content
composeTestRule.onNodeWithTag("recyclerView").performScrollToIndex(1)

// Pause to let the observer see the final state before the next test starts
Thread.sleep(2000)
}

@Test
fun testEdgewaterHotelCannedResponse() {
onView(withId(R.id.editTextMessage))
.perform(replaceText("Is the Edgewater Hotel in Seattle a good hotel?"), closeSoftKeyboard())
onView(withId(R.id.buttonSend)).perform(click())
Thread.sleep(7000)
composeTestRule
.onNodeWithTag("editTextMessage")
.performTextReplacement("Is the Edgewater Hotel in Seattle a good hotel?")
composeTestRule.onNodeWithTag("buttonSend").performClick()
composeTestRule.waitUntil(15_000) {
composeTestRule.onAllNodesWithTag("gmpA2UIView").fetchSemanticsNodes().isNotEmpty()
}

// Verify that the A2UIView container (which renders the mock JSON) is displayed
onView(withId(R.id.gmpA2UIView)).check(matches(isDisplayed()))
// Verify that the A2UIView container (which renders the mock JSON) exists
composeTestRule.onNodeWithTag("gmpA2UIView").assertExists()

// Scroll the recycler view to see the full content
onView(withId(R.id.recyclerView)).perform(swipeUp())
// Scroll the message list to see the full content
composeTestRule.onNodeWithTag("recyclerView").performScrollToIndex(1)

// Pause to let the observer see the final state before the next test starts
Thread.sleep(2000)
}

@Test
fun testKirklandCommuteCannedResponse() {
onView(withId(R.id.editTextMessage))
.perform(
replaceText(
"How long will it take to commute to Google Kirkland office from downtown Redmond during my morning rush hour commute?"
),
closeSoftKeyboard(),
composeTestRule
.onNodeWithTag("editTextMessage")
.performTextReplacement(
"How long will it take to commute to Google Kirkland office from downtown Redmond during my morning rush hour commute?"
)
onView(withId(R.id.buttonSend)).perform(click())
Thread.sleep(7000)
composeTestRule.onNodeWithTag("buttonSend").performClick()
composeTestRule.waitUntil(15_000) {
composeTestRule.onAllNodesWithTag("gmpA2UIView").fetchSemanticsNodes().isNotEmpty()
}

// Verify that the A2UIView container (which renders the mock JSON) is displayed
onView(withId(R.id.gmpA2UIView)).check(matches(isDisplayed()))
// Verify that the A2UIView container (which renders the mock JSON) exists
composeTestRule.onNodeWithTag("gmpA2UIView").assertExists()

// Scroll the recycler view to see the full content
onView(withId(R.id.recyclerView)).perform(swipeUp())
// Scroll the message list to see the full content
composeTestRule.onNodeWithTag("recyclerView").performScrollToIndex(1)

// Pause to let the observer see the final state before the next test starts
Thread.sleep(2000)
}

@Test
fun testSLUSaladsCannedResponse() {
onView(withId(R.id.editTextMessage))
.perform(
replaceText(
"Show me 5 lunch restaurants with Salads in South Lake Union. Give me directions to the 2nd one (starting from the Google South Lake Union WLK building)"
),
closeSoftKeyboard(),
composeTestRule
.onNodeWithTag("editTextMessage")
.performTextReplacement(
"Show me 5 lunch restaurants with Salads in South Lake Union. Give me directions to the 2nd one (starting from the Google South Lake Union WLK building)"
)
onView(withId(R.id.buttonSend)).perform(click())
Thread.sleep(7000)
composeTestRule.onNodeWithTag("buttonSend").performClick()
composeTestRule.waitUntil(15_000) {
composeTestRule.onAllNodesWithTag("gmpA2UIView").fetchSemanticsNodes().isNotEmpty()
}

// Verify that the A2UIView container (which renders the mock JSON) is displayed
onView(withId(R.id.gmpA2UIView)).check(matches(isDisplayed()))
// Verify that the A2UIView container (which renders the mock JSON) exists
composeTestRule.onNodeWithTag("gmpA2UIView").assertExists()

// Scroll the recycler view to see the full content
onView(withId(R.id.recyclerView)).perform(swipeUp())
// Scroll the message list to see the full content
composeTestRule.onNodeWithTag("recyclerView").performScrollToIndex(1)

// Pause to let the observer see the final state before the next test starts
Thread.sleep(2000)
}

@Test
fun testLondonItineraryCannedResponse() {
onView(withId(R.id.editTextMessage))
.perform(
replaceText("Give me a 3 day itinerary for a family of 3 traveling to London"),
closeSoftKeyboard(),
)
onView(withId(R.id.buttonSend)).perform(click())
Thread.sleep(7000)

// Verify that the A2UIView container (which renders the mock JSON) is displayed
onView(withId(R.id.gmpA2UIView)).check(matches(isDisplayed()))

// Scroll the recycler view to see the full content
onView(withId(R.id.recyclerView)).perform(swipeUp())
composeTestRule
.onNodeWithTag("editTextMessage")
.performTextReplacement("Give me a 3 day itinerary for a family of 3 traveling to London")
composeTestRule.onNodeWithTag("buttonSend").performClick()
composeTestRule.waitUntil(15_000) {
composeTestRule.onAllNodesWithTag("gmpA2UIView").fetchSemanticsNodes().isNotEmpty()
}

// Verify that the A2UIView container (which renders the mock JSON) exists
composeTestRule.onNodeWithTag("gmpA2UIView").assertExists()

// Scroll the message list to see the full content
composeTestRule.onNodeWithTag("recyclerView").performScrollToIndex(1)

// Pause to let the observer see the final state before the next test starts
Thread.sleep(2000)
Expand Down
2 changes: 0 additions & 2 deletions client/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.maui">



<uses-permission android:name="android.permission.INTERNET" />

<application
Expand Down
Loading
Loading