Refine mobile Assistant interaction layout

This commit is contained in:
Kaede Akino committed 2026-09-13 16:36:45 +08:00
1 parent f14ec61cec
commit dc7e5ca851
4 files changed
+67 -62

No files matched your search

@@ -381,25 +381,11 @@ private struct MobileAssistantSteeringQueue: View {
@State private var rowsHeight: CGFloat = 0
private var deliveryStatus: LocalizedStringKey {
switch conversation.runState {
case .waitingForApproval, .waitingForUser: "Waiting for response"
default: "Next step"
}
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack(spacing: 8) {
Label("Steering", systemImage: "arrow.turn.down.right")
.font(.caption.weight(.medium))
.foregroundStyle(.secondary)
Spacer(minLength: 8)
Text(deliveryStatus)
.font(.caption2)
.foregroundStyle(.tertiary)
.lineLimit(1)
}
Label("Steering", systemImage: "arrow.turn.down.right")
.font(.caption.weight(.medium))
.foregroundStyle(.secondary)
ScrollView {
VStack(alignment: .leading, spacing: 0) {
@@ -442,8 +428,8 @@ private struct MobileAssistantSteeringQueue: View {
}
Image(systemName: "xmark")
.font(.system(size: 8, weight: .bold))
.frame(width: 20, height: 20)
.font(.caption.weight(.bold))
.frame(width: 28, height: 28)
.foregroundStyle(.secondary)
}
.padding(.horizontal, 8)
@@ -558,6 +544,14 @@ struct MobileAssistantConversation: View {
}
@ViewBuilder private var bottomSurface: some View {
bottomSurfaceContent
.padding(12)
.frame(maxWidth: 840)
.frame(maxWidth: .infinity)
.background(Color(uiColor: .systemBackground))
}
@ViewBuilder private var bottomSurfaceContent: some View {
if conversation.isArchived {
archivedNotice
.padding(12)
@@ -566,15 +560,10 @@ struct MobileAssistantConversation: View {
RoundedRectangle(cornerRadius: 18, style: .continuous)
.stroke(.primary.opacity(0.08), lineWidth: 1)
}
.padding(12)
} else if conversation.pendingApproval != nil || conversation.pendingUserInteraction != nil {
MobileAssistantInteraction(conversation: conversation, hosts: app.library)
.padding(12)
.background(Color(uiColor: .systemBackground))
} else {
regularInteractionSurface
.padding(12)
.background(Color(uiColor: .systemBackground))
}
}
@@ -140,6 +140,7 @@ struct MobileWorkspaceListView: View {
Image(systemName: row.entry.kind.symbol)
.foregroundStyle(.tint)
.frame(width: 28, height: 28)
.accessibilityLabel(Text(row.entry.kind.label))
VStack(alignment: .leading, spacing: 2) {
Text(row.host.name).foregroundStyle(.primary).lineLimit(1)
Text(app.library.loginAddress(for: row.host))
@@ -149,13 +150,10 @@ struct MobileWorkspaceListView: View {
.truncationMode(.middle)
}
Spacer(minLength: 12)
VStack(alignment: .trailing, spacing: 2) {
Text(row.entry.kind.label).font(.caption.weight(.medium))
Text(row.entry.connectedAt, format: .relative(presentation: .numeric).locale(locale))
.font(.caption2)
}
.foregroundStyle(.secondary)
.lineLimit(1)
Text(row.entry.connectedAt, format: .relative(presentation: .numeric).locale(locale))
.font(.caption2)
.foregroundStyle(.secondary)
.lineLimit(1)
}
.frame(maxWidth: .infinity, alignment: .leading)
.contentShape(.rect)
+46 -9
View File
@@ -27,8 +27,11 @@ struct MobileNavigationParityTests {
assistant.components(separatedBy: "@ViewBuilder private var conversationTitleMenu").last?
.components(separatedBy: "private func titleMenuButton").first)
let bottomSurface = try #require(
transcript.components(separatedBy: "@ViewBuilder private var bottomSurface").last?
.components(separatedBy: "private var archivedNotice").first)
transcript.components(separatedBy: "@ViewBuilder private var bottomSurface: some View {").last?
.components(separatedBy: "@ViewBuilder private var bottomSurfaceContent").first)
let transcriptRows = try #require(
transcript.components(separatedBy: "MobileNativeList(").last?
.components(separatedBy: ".safeAreaInset(edge: .bottom").first)
let assistantRows = try #require(
assistant.components(separatedBy: "private func conversationRow").last?
.components(separatedBy: "private func select").first)
@@ -93,6 +96,7 @@ struct MobileNavigationParityTests {
#expect(transcript.contains("AssistantTranscriptPresentationState()"))
#expect(transcript.contains("DisclosureGroup(isExpanded: disclosureBinding"))
#expect(!transcript.contains("LazyVStack"))
#expect(transcriptRows.contains(".frame(maxWidth: 840)"))
#expect(workRows.contains("if batch.cards.count == 1, let card = batch.cards.first"))
#expect(workRows.contains("defaultExpanded: true"))
#expect(workRows.contains("presentation.isExpanded(.toolBatch(batch.id))"))
@@ -110,12 +114,15 @@ struct MobileNavigationParityTests {
let interaction = try String(
contentsOf: root.appending(path: "TFI/Features/AI/MobileAssistantInteraction.swift"), encoding: .utf8)
let bottomSurface = try #require(
conversation.components(separatedBy: "@ViewBuilder private var bottomSurface").last?
.components(separatedBy: "private var archivedNotice").first)
conversation.components(separatedBy: "@ViewBuilder private var bottomSurface: some View {").last?
.components(separatedBy: "@ViewBuilder private var bottomSurfaceContent").first)
let bottomSurfaceContent = try #require(
conversation.components(separatedBy: "@ViewBuilder private var bottomSurfaceContent").last?
.components(separatedBy: "private var regularInteractionSurface").first)
let blockingSurface = try #require(
bottomSurface.components(separatedBy: "conversation.pendingUserInteraction != nil {").last?
bottomSurfaceContent.components(separatedBy: "conversation.pendingUserInteraction != nil {").last?
.components(separatedBy: "} else {").first)
let composerSurface = try #require(bottomSurface.components(separatedBy: "} else {").last)
let composerSurface = try #require(bottomSurfaceContent.components(separatedBy: "} else {").last)
let regularSurface = try #require(
conversation.components(separatedBy: "private var regularInteractionSurface").last?
.components(separatedBy: "private var archivedNotice").first
@@ -126,13 +133,22 @@ struct MobileNavigationParityTests {
)
#expect(
bottomSurface.contains(
bottomSurfaceContent.contains(
"conversation.pendingApproval != nil || conversation.pendingUserInteraction != nil"))
#expect(bottomSurface.contains("bottomSurfaceContent"))
let padding = try #require(bottomSurface.range(of: ".padding(12)"))
let widthLimit = try #require(bottomSurface.range(of: ".frame(maxWidth: 840)"))
let fullWidth = try #require(bottomSurface.range(of: ".frame(maxWidth: .infinity)"))
let background = try #require(
bottomSurface.range(of: ".background(Color(uiColor: .systemBackground))"))
#expect(padding.lowerBound < widthLimit.lowerBound)
#expect(widthLimit.lowerBound < fullWidth.lowerBound)
#expect(fullWidth.lowerBound < background.lowerBound)
#expect(bottomSurfaceContent.contains("if conversation.isArchived"))
#expect(blockingSurface.contains("MobileAssistantInteraction("))
#expect(!blockingSurface.contains("MobileAssistantComposer("))
#expect(!blockingSurface.contains("MobileAssistantTaskCapsule("))
#expect(!blockingSurface.contains("MobileAssistantSteeringQueue("))
#expect(blockingSurface.contains(".background(Color(uiColor: .systemBackground))"))
#expect(composerSurface.contains("regularInteractionSurface"))
#expect(regularSurface.contains("ZStack(alignment: .top)"))
#expect(regularFlow.contains("MobileAssistantSteeringQueue("))
@@ -141,7 +157,7 @@ struct MobileNavigationParityTests {
#expect(regularSurface.contains("MobileAssistantTaskCapsule("))
#expect(regularSurface.contains(".offset(y: -44)"))
#expect(!composerSurface.contains("MobileAssistantInteraction("))
#expect(composerSurface.contains(".background(Color(uiColor: .systemBackground))"))
#expect(!bottomSurfaceContent.contains(".background(Color(uiColor: .systemBackground))"))
#expect(interaction.contains("conversation.answerUserInputOption(option.id, in: questionID)"))
#expect(!interaction.contains("conversation.toggleUserInputOption(option.id, in: questionID)"))
#expect(interaction.components(separatedBy: "MobileAssistantAdaptiveHeight {").count == 3)
@@ -186,6 +202,10 @@ struct MobileNavigationParityTests {
contentsOf: root.appending(path: "TFI/Features/AI/MobileAssistantTranscriptCards.swift"),
encoding: .utf8
)
let desktopConversation = try String(
contentsOf: root.appending(path: "TFM/Features/AI/ConversationTranscriptView.swift"),
encoding: .utf8
)
#expect(!composer.contains("pendingSteering"))
#expect(conversation.contains("struct MobileAssistantSteeringQueue"))
@@ -197,7 +217,17 @@ struct MobileNavigationParityTests {
.components(separatedBy: "struct MobileAssistantConversation").first
)
#expect(steeringQueue.contains("Image(systemName: \"xmark\")"))
#expect(steeringQueue.contains(".font(.caption.weight(.bold))"))
#expect(steeringQueue.contains(".frame(width: 28, height: 28)"))
#expect(!steeringQueue.contains(".onHover"))
let desktopSteeringQueue = try #require(
desktopConversation.components(separatedBy: "struct SteeringQueueView").last)
for source in [steeringQueue, desktopSteeringQueue] {
#expect(!source.contains("\"Next step\""))
#expect(!source.contains("\"Waiting for response\""))
#expect(!source.contains("deliveryStatus"))
#expect(!source.contains("isWaitingForResponse"))
}
#expect(conversation.contains("struct MobileAssistantTaskCapsule"))
#expect(conversation.contains("ProgressRing("))
#expect(conversation.contains(".offset(y: -overlayHeight)"))
@@ -748,6 +778,13 @@ struct MobileNavigationParityTests {
#expect(workspaceList.contains("description: Text(\"Use Connect to start a workspace.\")"))
#expect(workspaceList.contains("Section(\"History\")"))
#expect(workspaceList.contains("static let historyLimit = 5"))
let historyRows = try #require(
workspaceList.components(separatedBy: "private var historyRowsContent").last?
.components(separatedBy: "private func statusTint").first)
#expect(historyRows.contains("Image(systemName: row.entry.kind.symbol)"))
#expect(historyRows.contains(".accessibilityLabel(Text(row.entry.kind.label))"))
#expect(historyRows.contains("Text(row.entry.connectedAt, format: .relative"))
#expect(!historyRows.contains("Text(row.entry.kind.label).font"))
#expect(!workspaceList.contains("Open a host from Vault to start a workspace."))
#expect(!workspaceList.contains(".buttonStyle(.borderedProminent)"))
#expect(workspaceList.contains(".buttonStyle(.plain)"))
@@ -881,30 +881,11 @@ struct SteeringQueueView: View {
@State private var rowsHeight: CGFloat = 0
@State private var hoveredMessageID: PendingSteeringMessage.ID?
/// The wait has two different causes, and only one of them resolves on its
/// own. While a card above is holding the run, saying "next step" would be
/// a promise nothing is working towards.
private var deliveryStatus: LocalizedStringKey {
switch conversation.runState {
case .waitingForApproval, .waitingForUser: "Waiting for response"
default: "Next step"
}
}
var body: some View {
VStack(alignment: .leading, spacing: Metrics.Space.m) {
HStack(spacing: Metrics.Space.s) {
Label("Steering", systemImage: "arrow.turn.down.right")
.font(.caption.weight(.medium))
.foregroundStyle(.secondary)
Spacer(minLength: Metrics.Space.s)
Text(deliveryStatus)
.font(.caption2)
.foregroundStyle(.tertiary)
.lineLimit(1)
}
Label("Steering", systemImage: "arrow.turn.down.right")
.font(.caption.weight(.medium))
.foregroundStyle(.secondary)
ScrollView {
VStack(alignment: .leading, spacing: 0) {