Skip to content
Merged
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
7 changes: 7 additions & 0 deletions .agent_memory/session_context.archive.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@
# Older handover entries rotated out of session_context.md. Not read by default.
# Consult only if you need historical detail on a specific past change.

## 2026-05-21 — Fixed Chirpy Assistant invalid model name and enhanced failure fallback suggestions
- Fixed a 404/Unknown inference error by updating `GeminiNanoDocAssistant.kt`'s `MODEL_NAME` from `"gemini-3.1-flash-lite"` to the correct Firebase AI Logic preview name `"gemini-3.1-flash-lite-preview"`.
- Overhauled multi-turn hybrid chat seeding: eliminated the redundant background `chat.sendMessage` call on the first turn; if the first turn is answered on-device, the session caches the Q&A locally and seeds the subsequent cloud-chat session via `startChat(history = ...)`.
- Expanded the hybrid model's `looksLikeNoAnswer` heuristics to better detect on-device failure and fall back to the grounded cloud model.
- Programmed a smart UI fallback: on inference error (offline, rate limit, model not found), Chirpy displays local keyword search results as recommended page chips.
- Verified 100% compliance with Spotless, Detekt, and unit tests (`:feature:docs:allTests` and `:androidApp:testGoogleDebugUnitTest`).

## 2026-05-20 — Decoupled and Isolated Flatpak manifest generation logic to build-logic/flatpak
- Isolated the optimized `GenerateFlatpakSourcesTask` from monolithic `build-logic/convention` into its own specialized, lightweight `:flatpak` subproject under `build-logic`.
- Created `:flatpak` configuration and registered the formal plugin ID `"meshtastic.flatpak"` implemented by `FlatpakConventionPlugin` inside the default package namespace.
Expand Down
13 changes: 6 additions & 7 deletions .agent_memory/session_context.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@
# the oldest entries to `session_context.archive.md` (not read by default). The
# "Golden Context" block at the bottom is stable across sessions; keep it here.

## 2026-06-03 — Cluster-marker FATAL: revert shipped map series + in-scope rememberComposeBitmapDescriptor fix
- Reverted ALL google-flavor map changes to before #5684 (per user): restored MapView.kt, NodeClusterMarkers.kt, WaypointMarkers.kt, InlineMap.kt to parent commit bc9f1637; deleted MarkerBitmapRenderer.kt; re-pinned `play-services-maps = 20.0.0` in libs.versions.toml. The shipped #5702–#5719 series (Canvas markers + ViewTree-owner band-aids) had lost the info-window popups + interactions.
- Root cause (verified against maps-compose 8.3.0 + android-maps-utils 4.1.1 SOURCE in gradle cache): ONLY `Clustering(clusterItemContent=…)` crashes — its `ComposeUiClusterRenderer` builds a *detached* `InvalidatingComposeView` with a fake lifecycle owner and NO SavedStateRegistryOwner. `MarkerComposable` already bakes its icon via the safe in-scope `rememberComposeBitmapDescriptor`; info windows render with the live marker compositionContext. So InlineMap/NodeTrack/Traceroute were left untouched.
- Fix (NodeClusterMarkers.kt ONLY): icons baked in-scope via `rememberComposeBitmapDescriptor(node){ PulsingNodeChip }` into a snapshot stateMap; custom `private class NodeClusterRenderer : DefaultClusterRenderer` assigns them in onBeforeClusterItemRendered/onClusterItemUpdated (bg thread, READ-only — never composes, so the crash class is gone). Native info windows (super sets title/snippet) + onClusterItemInfoWindowClick→navigateToNodeDetails; precision circles drawn from the renderer's own `unclusteredItems` MutableState (clusterItemDecoration can't fire — `ClusterRendererItemState` is lib-internal). Strictly better than the elegant-euler Canvas branch — keeps the REAL Compose chip.
- `compileGoogleDebugKotlin` + `spotlessCheck` + `detekt` PASS. NOT committed, NOT device-verified. Next: device-test (clusters show chips + info-window popups + no FATAL), then commit/push.

## 2026-05-28 — Stabilized DatabaseManager withDb retry host test
- Hardened `DatabaseManagerWithDbRetryTest` to remove CI race conditions by running the manager on a `StandardTestDispatcher(testScheduler)` instead of real `Dispatchers.IO`.
- Added a `withTimeout(10_000)` guard around the test body to fail fast on coordination stalls instead of hanging/flapping.
Expand Down Expand Up @@ -33,13 +39,6 @@
- Refactored `GeminiNanoDocAssistant.answer` to reuse `answerStream` flow under the hood, eliminating duplicate prompting code.
- Verified that all unit tests (`:feature:docs:allTests`) and static analysis checks (`spotlessApply spotlessCheck detekt`) pass 100% green.

## 2026-05-21 — Fixed Chirpy Assistant invalid model name and enhanced failure fallback suggestions
- Fixed a 404/Unknown inference error by updating `GeminiNanoDocAssistant.kt`'s `MODEL_NAME` from `"gemini-3.1-flash-lite"` to the correct Firebase AI Logic preview name `"gemini-3.1-flash-lite-preview"`.
- Overhauled multi-turn hybrid chat seeding: eliminated the redundant background `chat.sendMessage` call on the first turn; if the first turn is answered on-device, the session caches the Q&A locally and seeds the subsequent cloud-chat session via `startChat(history = ...)`.
- Expanded the hybrid model's `looksLikeNoAnswer` heuristics to better detect on-device failure and fall back to the grounded cloud model.
- Programmed a smart UI fallback: on inference error (offline, rate limit, model not found), Chirpy displays local keyword search results as recommended page chips.
- Verified 100% compliance with Spotless, Detekt, and unit tests (`:feature:docs:allTests` and `:androidApp:testGoogleDebugUnitTest`).

## Golden Context (stable across sessions)
- Always check `.skills/compose-ui/strings-index.txt` before reading `strings.xml`.
- Run `python3 scripts/sort-strings.py` after adding strings to keep the index organized.
Expand Down
56 changes: 18 additions & 38 deletions androidApp/src/google/kotlin/org/meshtastic/app/map/MapView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ import com.google.maps.android.compose.MapProperties
import com.google.maps.android.compose.MapType
import com.google.maps.android.compose.MapUiSettings
import com.google.maps.android.compose.MapsComposeExperimentalApi
import com.google.maps.android.compose.Marker
import com.google.maps.android.compose.MarkerComposable
import com.google.maps.android.compose.MarkerInfoWindowComposable
import com.google.maps.android.compose.Polyline
import com.google.maps.android.compose.TileOverlay
Expand All @@ -102,7 +102,6 @@ import org.meshtastic.app.map.component.MapTypeDropdown
import org.meshtastic.app.map.component.NodeClusterMarkers
import org.meshtastic.app.map.component.NodeMapFilterDropdown
import org.meshtastic.app.map.component.WaypointMarkers
import org.meshtastic.app.map.component.rememberNodeChipDescriptor
import org.meshtastic.app.map.model.NodeClusterItem
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.common.util.nowSeconds
Expand All @@ -126,6 +125,7 @@ import org.meshtastic.core.resources.sats
import org.meshtastic.core.resources.speed
import org.meshtastic.core.resources.timestamp
import org.meshtastic.core.resources.track_point
import org.meshtastic.core.ui.component.NodeChip
import org.meshtastic.core.ui.icon.Layers
import org.meshtastic.core.ui.icon.Map
import org.meshtastic.core.ui.icon.MeshtasticIcons
Expand Down Expand Up @@ -186,7 +186,6 @@ fun MapView(
mode: GoogleMapMode = GoogleMapMode.Main,
) {
val context = LocalContext.current

val coroutineScope = rememberCoroutineScope()
val mapLayers by mapViewModel.mapLayers.collectAsStateWithLifecycle()

Expand Down Expand Up @@ -493,31 +492,7 @@ fun MapView(
val onRemoveLayer = { layerId: String -> mapViewModel.removeMapLayer(layerId) }
val onToggleVisibility = { layerId: String -> mapViewModel.toggleLayerVisibility(layerId) }

// Resolve the selected custom tile provider once (cached). getTileProvider returns null when the
// configured source is unusable (bad {x}/{y}/{z} URL template, missing local MBTiles file, etc.).
val customTileConfigs by mapViewModel.customTileProviderConfigs.collectAsStateWithLifecycle()
val customTileProvider =
remember(currentCustomTileProviderUrl, customTileConfigs) {
currentCustomTileProviderUrl?.let { url ->
val config = customTileConfigs.find { it.urlTemplate == url || it.localUri == url }
mapViewModel.getTileProvider(config)
}
}

// Only blank the Google base map (MapType.NONE) when we actually have a working custom basemap to draw
// over it. If the selected custom source failed to build, fall back to the user's base map instead of
// rendering MapType.NONE with no tiles — that is a solid black screen with no recourse.
val effectiveGoogleMapType = if (customTileProvider != null) MapType.NONE else selectedGoogleMapType

// Surface the fallback so a broken custom tile source is diagnosable instead of a silent black map.
LaunchedEffect(currentCustomTileProviderUrl, customTileProvider) {
if (currentCustomTileProviderUrl != null && customTileProvider == null) {
Logger.withTag("MapView").w {
"Custom tile provider '$currentCustomTileProviderUrl' could not be built; " +
"falling back to base map $selectedGoogleMapType"
}
}
}
val effectiveGoogleMapType = if (currentCustomTileProviderUrl != null) MapType.NONE else selectedGoogleMapType

var showClusterItemsDialog by remember { mutableStateOf<List<NodeClusterItem>?>(null) }

Expand Down Expand Up @@ -566,11 +541,16 @@ fun MapView(
}
},
) {
// Custom tile overlay (all modes) — uses the hoisted provider so the base-map decision above and
// this overlay stay consistent (no overlay ⇒ base map is shown, never a black MapType.NONE).
// Custom tile overlay (all modes)
key(currentCustomTileProviderUrl) {
customTileProvider?.let { tileProvider ->
TileOverlay(tileProvider = tileProvider, fadeIn = true, transparency = 0f, zIndex = -1f)
currentCustomTileProviderUrl?.let { url ->
val config =
mapViewModel.customTileProviderConfigs.collectAsStateWithLifecycle().value.find {
it.urlTemplate == url || it.localUri == url
}
mapViewModel.getTileProvider(config)?.let { tileProvider ->
TileOverlay(tileProvider = tileProvider, fadeIn = true, transparency = 0f, zIndex = -1f)
}
}
}

Expand Down Expand Up @@ -884,17 +864,17 @@ private fun NodeTrackOverlay(
}

if (index == sortedPositions.lastIndex) {
val chipIcon = rememberNodeChipDescriptor(focusedNode)
Marker(
MarkerComposable(
state = markerState,
icon = chipIcon,
zIndex = activeNodeZIndex,
alpha = if (isHighPriority) 1.0f else 0.9f,
onClick = {
onPositionSelected?.invoke(position.time)
false // Allow default info window behavior
},
)
) {
NodeChip(node = focusedNode)
}
} else {
MarkerInfoWindowComposable(
state = markerState,
Expand Down Expand Up @@ -989,6 +969,7 @@ private fun speedFromPosition(position: Position, displayUnits: DisplayUnits): S

// region --- Traceroute Map Content ---

@OptIn(MapsComposeExperimentalApi::class)
@Composable
private fun TracerouteMapContent(
forwardOffsetPoints: List<LatLng>,
Expand Down Expand Up @@ -1017,8 +998,7 @@ private fun TracerouteMapContent(
}
displayNodes.forEach { node ->
val markerState = rememberUpdatedMarkerState(position = node.position.toLatLng())
val chipIcon = rememberNodeChipDescriptor(node)
Marker(state = markerState, icon = chipIcon, zIndex = 4f)
MarkerComposable(state = markerState, zIndex = 4f) { NodeChip(node = node) }
}
}

Expand Down

This file was deleted.

Loading