Skip to content

Commit 377c626

Browse files
committed
feat(explore): ordinal tab mapping for icon-only tab bars
Maps APP.md tab names to tab_bar zone elements sorted by X when text match fails.
1 parent b5a0a51 commit 377c626

1 file changed

Lines changed: 37 additions & 6 deletions

File tree

Sources/mirroir-mcp/BFSExplorerHelpers.swift

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -117,22 +117,26 @@ extension BFSExplorer {
117117
return plan
118118
}
119119

120-
/// Find OCR elements matching APP.md tab names by case-insensitive substring match.
121-
/// Returns ranked elements with breadth_navigation priority.
120+
/// Find OCR elements matching APP.md tab names.
121+
///
122+
/// Primary strategy: case-insensitive substring text match (works when tab bar has labels).
123+
/// Fallback: ordinal position mapping in the tab_bar zone (for icon-only tab bars like
124+
/// Instagram). Sorts tab_bar-zone elements by X coordinate and maps them to APP.md tab
125+
/// names by index — the developer's tab list order matches the visual left-to-right order.
122126
private func findTabTargets(
123127
tabNames: [String], elements: [TapPoint], visitedElements: Set<String>
124128
) -> [RankedElement] {
125129
var results: [RankedElement] = []
126-
let visited = visitedElements
130+
var matchedTabs: Set<String> = []
127131

132+
// Strategy 1: text matching
128133
for tabName in tabNames {
129134
let tabLower = tabName.lowercased()
130-
// Find the best matching element for this tab name
131135
guard let match = elements.first(where: { el in
132136
let elLower = el.text.lowercased()
133137
return (elLower == tabLower || elLower.contains(tabLower) || tabLower.contains(elLower))
134-
&& !visited.contains(el.text)
135-
&& !visited.contains(tabName)
138+
&& !visitedElements.contains(el.text)
139+
&& !visitedElements.contains(tabName)
136140
}) else { continue }
137141

138142
results.append(RankedElement(
@@ -142,6 +146,33 @@ extension BFSExplorer {
142146
displayLabel: tabName,
143147
isBreadthNavigation: true
144148
))
149+
matchedTabs.insert(tabName)
150+
}
151+
152+
// Strategy 2: ordinal mapping for unmatched tabs (icon-only tab bars)
153+
let unmatchedTabs = tabNames.filter { !matchedTabs.contains($0) }
154+
if !unmatchedTabs.isEmpty {
155+
let tabBarTop = windowSize.height * 0.88 // bottom 12% is tab_bar zone
156+
let tabBarElements = elements
157+
.filter { $0.tapY >= tabBarTop }
158+
.sorted { $0.tapX < $1.tapX }
159+
160+
if tabBarElements.count >= tabNames.count {
161+
// Map each tab name to the element at its index, skipping already-matched
162+
for (index, tabName) in tabNames.enumerated() {
163+
guard !matchedTabs.contains(tabName),
164+
index < tabBarElements.count,
165+
!visitedElements.contains(tabName) else { continue }
166+
let target = tabBarElements[index]
167+
results.append(RankedElement(
168+
point: target,
169+
score: ScreenPlanner.breadthRoleWeight + ScreenPlanner.highPriorityWeight,
170+
reason: "tab-ordinal(\(tabName)@\(index))",
171+
displayLabel: tabName,
172+
isBreadthNavigation: true
173+
))
174+
}
175+
}
145176
}
146177

147178
return results

0 commit comments

Comments
 (0)