TFI Hover skills & plan

This commit is contained in:
Kaede Akino committed 2026-09-13 09:55:58 +08:00
1 parent 3e040cc43b
commit 9a778d81d5
2 files changed
+68 -16

No files matched your search

+31 -13
View File
@@ -26,6 +26,27 @@ extension View {
}
}
private struct MobileDismissibleComposerBadge: View {
let symbol: String
let help: Text
let accessibilityLabel: Text
let action: () -> Void
@State private var isHovering = false
var body: some View {
Button(action: action) {
Image(systemName: isHovering ? "xmark" : symbol)
.contentTransition(.symbolEffect(.replace))
.mobileComposerControlIcon(opacity: 0.62)
}
.buttonStyle(.plain)
.onHover { isHovering = $0 }
.help(help)
.accessibilityLabel(accessibilityLabel)
}
}
struct MobileAssistantComposer: View {
let pro: ProAccessController
let model: MobileAssistantModel
@@ -480,25 +501,22 @@ struct MobileAssistantComposer: View {
@ViewBuilder private var badges: some View {
if conversation.isPlanning {
Button {
MobileDismissibleComposerBadge(
symbol: "lightbulb.max",
help: Text("Plan mode — click to turn off"),
accessibilityLabel: Text("Plan mode on, turn off")
) {
conversation.isPlanning = false
} label: {
Label("Plan", systemImage: "lightbulb.max")
.labelStyle(.iconOnly)
.mobileComposerControlIcon(opacity: 0.62)
}
.buttonStyle(.plain)
}
if let skill = composer.armedSkillName {
Button {
MobileDismissibleComposerBadge(
symbol: "book.closed",
help: Text("\(skill) will be loaded for this message — click to remove"),
accessibilityLabel: Text("Skill \(skill) armed, remove")
) {
composer.armedSkillName = nil
} label: {
Label(skill, systemImage: "book.closed")
.labelStyle(.iconOnly)
.mobileComposerControlIcon(opacity: 0.62)
}
.buttonStyle(.plain)
.accessibilityLabel(Text("Skill \(skill) armed, remove"))
}
}
+37 -3
View File
@@ -350,10 +350,40 @@ struct MobileNavigationParityTests {
composer.components(separatedBy: "private var badges").last?
.components(separatedBy: "private var modelProviders").first)
#expect(badges.contains("armedSkillName"))
#expect(badges.components(separatedBy: ".labelStyle(.iconOnly)").count == 3)
#expect(badges.components(separatedBy: "MobileDismissibleComposerBadge(").count == 3)
#expect(composer.contains(".symbolRenderingMode(.monochrome)"))
}
@Test func assistantPlanAndSkillBadgesExposeDesktopHoverRemovalAffordance() throws {
let root = URL(fileURLWithPath: #filePath)
.deletingLastPathComponent()
.deletingLastPathComponent()
let composer = try String(
contentsOf: root.appending(path: "TFI/Features/AI/MobileAssistantComposer.swift"), encoding: .utf8)
let badge = try #require(
composer.components(separatedBy: "private struct MobileDismissibleComposerBadge").last?
.components(separatedBy: "struct MobileAssistantComposer").first)
#expect(badge.contains("@State private var isHovering = false"))
#expect(badge.contains("isHovering ? \"xmark\" : symbol"))
#expect(badge.contains(".contentTransition(.symbolEffect(.replace))"))
#expect(badge.contains(".mobileComposerControlIcon(opacity: 0.62)"))
#expect(badge.contains(".onHover { isHovering = $0 }"))
#expect(badge.contains(".help(help)"))
#expect(badge.contains(".accessibilityLabel(accessibilityLabel)"))
let badges = try #require(
composer.components(separatedBy: "@ViewBuilder private var badges").last?
.components(separatedBy: "private var modelProviders").first)
#expect(badges.contains("symbol: \"lightbulb.max\""))
#expect(badges.contains("conversation.isPlanning = false"))
#expect(badges.contains("Text(\"Plan mode — click to turn off\")"))
#expect(badges.contains("Text(\"Plan mode on, turn off\")"))
#expect(badges.contains("symbol: \"book.closed\""))
#expect(badges.contains("composer.armedSkillName = nil"))
#expect(badges.contains("Skill \\(skill) armed, remove"))
}
@Test func assistantPermissionMenuUsesLeadingCheckmarkAndKeepsItsTriggerVisible() throws {
let root = URL(fileURLWithPath: #filePath)
.deletingLastPathComponent()
@@ -439,7 +469,6 @@ struct MobileNavigationParityTests {
("private var addMenu", "private var permissionMenu"),
("private var permissionMenu", "private var resolvedPermissionMode"),
("private var microphoneButton", "@ViewBuilder private var primaryButton"),
("@ViewBuilder private var badges", "private var modelProviders"),
] {
let controls = try #require(
composer.components(separatedBy: markers.0).last?
@@ -450,7 +479,12 @@ struct MobileNavigationParityTests {
let badges = try #require(
composer.components(separatedBy: "@ViewBuilder private var badges").last?
.components(separatedBy: "private var modelProviders").first)
#expect(badges.components(separatedBy: ".mobileComposerControlIcon(opacity: 0.62)").count == 3)
#expect(badges.components(separatedBy: "MobileDismissibleComposerBadge(").count == 3)
let dismissibleBadge = try #require(
composer.components(separatedBy: "private struct MobileDismissibleComposerBadge").last?
.components(separatedBy: "struct MobileAssistantComposer").first)
#expect(dismissibleBadge.contains(".mobileComposerControlIcon(opacity: 0.62)"))
let primaryButton = try #require(
composer.components(separatedBy: "@ViewBuilder private var primaryButton").last?