TFI Hover skills & plan
This commit is contained in:
1 parent
3e040cc43b
commit
9a778d81d5
2 files changed
+68
-16
No files matched your search
@@ -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"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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?
|
||||
|
||||
Reference in new issue
Block a user