fix(ios): clarify agent chat session

This commit is contained in:
joshavant
2026-06-06 01:25:24 -05:00
parent 67dc71983c
commit e8c0d92015
4 changed files with 85 additions and 8 deletions

View File

@@ -285,7 +285,7 @@ extension AgentProTab {
Circle().strokeBorder(self.iconButtonStroke, lineWidth: 1)
}
}
.accessibilityLabel(isActive ? "Active agent" : "Make active agent")
.accessibilityLabel(isActive ? "Default agent" : "Set default agent")
}
.padding(.vertical, 14)
.padding(.horizontal, 13)

View File

@@ -5,6 +5,7 @@ struct CommandCenterTab: View {
@Environment(NodeAppModel.self) private var appModel
@Environment(\.colorScheme) private var colorScheme
@Environment(\.scenePhase) private var scenePhase
@State private var defaultChatSessionEntry: OpenClawChatSessionEntry?
@State private var recentChatSessions: [OpenClawChatSessionEntry] = []
var openChat: () -> Void
var openSettings: () -> Void
@@ -44,6 +45,7 @@ struct CommandCenterTab: View {
VStack(alignment: .leading, spacing: 10) {
self.header
self.gatewayCard
self.defaultChatSessionSection
self.pendingApprovals
self.recentSessions
self.liveActivity
@@ -152,6 +154,30 @@ struct CommandCenterTab: View {
.padding(.horizontal, 10)
}
private var defaultChatSessionSection: some View {
CommandPanel(padding: 0) {
VStack(spacing: 0) {
self.cardHeader(
title: "Agent session",
value: nil,
color: OpenClawBrand.accent)
.padding(.horizontal, 12)
.padding(.top, 10)
.padding(.bottom, 3)
Button {
self.open(.chat(nil))
} label: {
CommandSessionRow(item: self.defaultChatWorkItem)
}
.buttonStyle(.plain)
.padding(.horizontal, 10)
.padding(.bottom, 10)
}
}
.padding(.horizontal, OpenClawProMetric.pagePadding)
}
private var pendingApprovals: some View {
self.pendingApprovalsContent
.padding(.horizontal, OpenClawProMetric.pagePadding)
@@ -424,6 +450,27 @@ struct CommandCenterTab: View {
self.colorScheme == .dark ? Color.black.opacity(0.12) : Color.black.opacity(0.022)
}
private var defaultChatWorkItem: WorkItem {
let isOpen = self.appModel.chatSessionKey == self.appModel.defaultChatSessionKey
return WorkItem(
id: "default-chat",
icon: isOpen ? "bubble.left.and.text.bubble.right.fill" : "bubble.left.fill",
title: self.appModel.activeAgentName,
detail: self.defaultChatActivityText,
state: isOpen ? "open" : "default",
trailing: "chat",
color: isOpen ? OpenClawBrand.accent : OpenClawBrand.ok,
progress: nil,
route: .chat(nil))
}
private var defaultChatActivityText: String {
guard let updatedAt = self.defaultChatSessionEntry?.updatedAt, updatedAt > 0 else {
return "No recent activity"
}
return Self.relativeTimeText(forMilliseconds: updatedAt)
}
private var recentSessionRows: [WorkItem] {
self.sessionItems
}
@@ -485,6 +532,7 @@ struct CommandCenterTab: View {
let currentSessionKey = self.appModel.chatSessionKey
return self.recentChatSessions
.filter { !Self.isHiddenInternalSession($0.key) }
.filter { $0.key != self.appModel.defaultChatSessionKey }
.map { session in
Self.sessionWorkItem(for: session, currentSessionKey: currentSessionKey)
}
@@ -503,6 +551,9 @@ struct CommandCenterTab: View {
private func refreshRecentSessionsIfNeeded() async {
guard self.scenePhase == .active else { return }
guard self.appModel.isOperatorGatewayConnected else {
if self.defaultChatSessionEntry != nil {
self.defaultChatSessionEntry = nil
}
if !self.recentChatSessions.isEmpty {
self.recentChatSessions = []
}
@@ -512,29 +563,38 @@ struct CommandCenterTab: View {
do {
let transport = IOSGatewayChatTransport(gateway: appModel.operatorSession)
let response = try await transport.listSessions(limit: 20)
self.defaultChatSessionEntry = response.sessions.first {
$0.key == self.appModel.defaultChatSessionKey
}
self.recentChatSessions = Self.sessionChoices(
response.sessions,
currentSessionKey: self.appModel.chatSessionKey)
currentSessionKey: self.appModel.chatSessionKey,
defaultSessionKey: self.appModel.defaultChatSessionKey)
} catch {
self.defaultChatSessionEntry = nil
self.recentChatSessions = []
}
}
private static func sessionChoices(
_ sessions: [OpenClawChatSessionEntry],
currentSessionKey: String) -> [OpenClawChatSessionEntry]
currentSessionKey: String,
defaultSessionKey: String) -> [OpenClawChatSessionEntry]
{
let sorted = sessions.sorted { ($0.updatedAt ?? 0) > ($1.updatedAt ?? 0) }
var result: [OpenClawChatSessionEntry] = []
var included = Set<String>()
if let current = sorted.first(where: { $0.key == currentSessionKey }) {
if currentSessionKey != defaultSessionKey,
let current = sorted.first(where: { $0.key == currentSessionKey })
{
result.append(current)
included.insert(current.key)
}
for session in sorted {
guard !included.contains(session.key) else { continue }
guard session.key != defaultSessionKey else { continue }
guard !Self.isHiddenInternalSession(session.key) else { continue }
result.append(session)
included.insert(session.key)
@@ -554,7 +614,7 @@ struct CommandCenterTab: View {
icon: isCurrent ? "bubble.left.and.text.bubble.right.fill" : "bubble.left.fill",
title: Self.sessionTitle(session),
detail: Self.sessionDetail(session),
state: isCurrent ? "current" : "recent",
state: isCurrent ? "open" : "recent",
trailing: "chat",
color: isCurrent ? OpenClawBrand.accent : OpenClawBrand.ok,
progress: nil,
@@ -765,6 +825,7 @@ private struct CommandSessionsScreen: View {
private var sessionRows: [CommandCenterTab.WorkItem] {
self.sessions
.filter { !CommandCenterTab.isHiddenInternalSession($0.key) }
.filter { $0.key != self.appModel.defaultChatSessionKey }
.sorted { ($0.updatedAt ?? 0) > ($1.updatedAt ?? 0) }
.map {
CommandCenterTab.sessionWorkItem(

View File

@@ -231,7 +231,7 @@ extension SettingsProTab {
Divider()
self.detailRow("Discovered", value: "\(self.gatewayController.gateways.count)")
Divider()
self.detailRow("Active Agent", value: self.appModel.activeAgentName)
self.detailRow("Default Agent", value: self.appModel.activeAgentName)
Divider()
self.detailRow("Agents", value: "\(self.appModel.gatewayAgents.count)")
}
@@ -499,7 +499,7 @@ extension SettingsProTab {
var agentSelectionCard: some View {
ProCard(radius: SettingsLayout.cardRadius) {
VStack(alignment: .leading, spacing: 10) {
Text("Active Agent")
Text("Default Agent")
.font(.subheadline.weight(.semibold))
Picker("Agent", selection: self.$selectedAgentPickerId) {
Text("Default").tag("")
@@ -510,7 +510,7 @@ extension SettingsProTab {
Text(name.isEmpty ? agent.id : name).tag(agent.id)
}
}
Text("Controls which agent Chat and Talk use.")
Text("Used for new Chat and Talk sessions.")
.font(.caption)
.foregroundStyle(.secondary)
}

View File

@@ -310,6 +310,22 @@ private final class MockBootstrapNotificationCenter: NotificationCentering, @unc
#expect(appModel.chatSessionKey == "incident-42")
}
@Test @MainActor func openingNilChatSessionClearsExplicitChatFocus() {
let appModel = NodeAppModel()
appModel.gatewayDefaultAgentId = "main"
appModel.setSelectedAgentId("rust-claw")
appModel.openChat(sessionKey: "incident-42")
appModel.openChat(sessionKey: nil)
#expect(appModel.chatSessionKey == SessionKey.makeAgentSessionKey(
agentId: "rust-claw",
baseKey: "main"))
appModel.setSelectedAgentId("main")
#expect(appModel.chatSessionKey == "main")
}
@Test @MainActor func execApprovalPromptPresentationTracksLatestNotificationTap() throws {
let appModel = NodeAppModel()
try appModel._test_presentExecApprovalPrompt(