Disable empty Assistant conversation switchers

This commit is contained in:
Kaede Akino committed 2026-09-19 09:20:22 +08:00
1 parent 2bf9c1de98
commit cf96a3ab8d
3 files changed
+97 -2

No files matched your search

+3 -1
View File
@@ -978,7 +978,9 @@ instead of independently reading placement or device capabilities. Workspace det
retain the five application tabs beside top bars and in vertical toolbars. Bottom tabs
stay hidden in SSH/SFTP details whenever the vertical toolbar is unavailable.
Workspace and Assistant share a `rectangle.stack` switcher in top and vertical bars,
with a title menu elsewhere.
with a title menu elsewhere. When Assistant has no pinned or active conversations,
its icon switcher stays visible but disabled, and its title has no menu. A sole current
conversation still enables switching; archived conversations do not.
These switchers select peers within the feature; they are separate
from the five application tabs. Assistant library pages always retain their titles.
Metrics uses a labeled icon menu in a vertical toolbar and a principal segmented picker
@@ -55,9 +55,10 @@ struct MobileAssistantView: View {
}
.labelStyle(.iconOnly)
.accessibilityValue(conversationTitle)
.disabled(!hasSelectableConversations)
}
.mobileProminentAction()
} else if libraryRoute == nil {
} else if libraryRoute == nil && hasSelectableConversations {
ToolbarTitleMenu { conversationTitleMenu }
}
}
@@ -157,6 +158,10 @@ struct MobileAssistantView: View {
toolbarLayout.usesIconSwitcher && libraryRoute == nil
}
private var hasSelectableConversations: Bool {
!model.library.pinnedConversations.isEmpty || !model.library.activeConversations.isEmpty
}
private var detail: some View {
MobileAssistantConversation(
app: app,
+88
View File
@@ -217,6 +217,94 @@ struct MobileAdaptiveLayoutTests {
}
}
@Test(arguments: ["en", "zh-Hans", "zh-Hant"])
func assistantSwitchersReflectAvailableConversationsAcrossToolbarLayouts(language: String) async throws {
let app = try MobileAppModel(inMemory: true)
let scene = MobileSceneModel(app: app)
scene.selectedDestination = .assistant
await scene.assistant.load()
let conversation = scene.assistant.conversation
conversation.composer.draft = "Keep the draft while switchers update"
let regions = MobileRegionTestState()
var features = MobileFeatureRegistry()
features.register(
MobileFeatureRegistration(id: .assistant, title: "Assistant", symbol: "sparkles") { app, scene in
AnyView(
MobileAssistantView(
app: app, model: scene.assistant,
libraryRoute: Binding(
get: { scene.assistantLibraryRoute }, set: { scene.assistantLibraryRoute = $0 })
)
.environment(\.mobileToolbarLayout, regions.toolbarLayout))
})
try await withRoot(app: app, scene: scene, language: language, features: features, initialSizeClass: .compact) {
controller in
let navigation = try #require(navigation(titled: "Assistant", in: controller))
let page = try #require(navigation.topViewController)
for (state, enabled) in [
("empty", false), ("current", true), ("pinned", true), ("archived", false), ("restored", true),
] {
switch state {
case "current":
conversation.appendUserTurn("The only conversation")
scene.assistant.library.insertAndSelect(conversation)
case "pinned": conversation.isPinned = true
case "archived":
conversation.isPinned = false
conversation.archivedAt = .now
case "restored": conversation.archivedAt = nil
default: break
}
// Check a data-only update before changing toolbar placement.
try await settle()
try expectAssistantSwitcher(in: navigation, layout: regions.toolbarLayout, enabled: enabled)
for (name, layout) in [
("title", MobileToolbarLayout()),
("top", MobileToolbarLayout(isTopBar: true)),
("vertical", MobileToolbarLayout(verticalEdge: .trailing)),
] {
regions.toolbarLayout = layout
try await settle()
try expectAssistantSwitcher(in: navigation, layout: layout, enabled: enabled)
#expect(navigation.topViewController === page)
#expect(scene.assistant.conversation === conversation)
#expect(conversation.composer.draft == "Keep the draft while switchers update")
let newConversation = try #require(
toolbarButton(namedOneOf: ["New Conversation", "新建对话", "新增對話"], in: navigation))
#expect(newConversation.isEnabled)
capture(controller, name: "assistant-switcher-\(state)-\(name)-\(language)")
}
if !enabled {
let menu: UIMenu
if #available(iOS 27.0, *) {
menu = UIMenu(children: [try #require(page.navigationItem.additionalOverflowItems)])
} else {
menu = try #require(toolbarButton(namedOneOf: ["More", "更多"], in: navigation)?.menu)
}
let actions = try await resolvedMenuActions(menu, in: page.view)
for names: Set<String> in [["Conversations", "对话", "對話"], ["Archived", "已归档", "已封存"]] {
let action = try #require(actions.first { names.contains($0.title) })
#expect(!action.attributes.contains(.disabled))
}
}
}
}
}
private func expectAssistantSwitcher(
in navigation: UINavigationController, layout: MobileToolbarLayout, enabled: Bool
) throws {
let page = try #require(navigation.topViewController)
let switcher = toolbarButton(namedOneOf: ["Conversations", "对话", "對話"], in: navigation)
if layout.usesIconSwitcher {
#expect(try #require(switcher).isEnabled == enabled)
} else {
#expect(switcher == nil)
#expect((page.navigationItem.titleMenuProvider != nil) == enabled)
#expect(visibleLabels(in: navigation.navigationBar).contains { $0.text == page.navigationItem.title })
}
}
@Test(arguments: ["en", "zh-Hans", "zh-Hant"])
func assistantTitleAdaptsWithoutLosingItsConversationMenuOrDrafts(language: String) async throws {
guard UIDevice.current.userInterfaceIdiom == .pad else { return }