fix(chat): restore canonical native session ordering and search (#113886)

This commit is contained in:
Peter Steinberger
2026-07-25 16:42:33 -07:00
committed by GitHub
parent 81b7a0a006
commit 9defe24ec2
2 changed files with 102 additions and 10 deletions
@@ -496,14 +496,11 @@ public enum ChatSessionSidebarModel {
// active row selectable instead of showing an empty selection.
entries.append(self.placeholder(key: currentSessionKey))
}
entries.sort { (($0.updatedAt ?? $0.lastActivityAt) ?? 0) > (($1.updatedAt ?? $1.lastActivityAt) ?? 0) }
let needle = query.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
guard !needle.isEmpty else { return entries }
return entries.filter { entry in
self.displayName(for: entry).lowercased().contains(needle) ||
entry.key.lowercased().contains(needle)
}
// Gateway, cached lists, iOS, and macOS must share the same pin
// chronology, stable key ties, and searchable session fields.
return OpenClawChatSessionListOrganizer.filter(
OpenClawChatSessionListOrganizer.organize(entries),
search: query)
}
private static func placeholder(key: String) -> OpenClawChatSessionEntry {
@@ -8,8 +8,12 @@ struct ChatSessionSidebarModelTests {
private func entry(
key: String,
displayName: String? = nil,
label: String? = nil,
subject: String? = nil,
sessionId: String? = nil,
updatedAt: Double? = nil,
pinned: Bool? = nil,
pinnedAt: Double? = nil,
archived: Bool? = nil,
unread: Bool? = nil,
lastReadAt: Double? = nil,
@@ -31,11 +35,11 @@ struct ChatSessionSidebarModelTests {
kind: nil,
displayName: displayName,
surface: nil,
subject: nil,
subject: subject,
room: nil,
space: nil,
updatedAt: updatedAt,
sessionId: nil,
sessionId: sessionId,
systemSent: nil,
abortedLastRun: nil,
thinkingLevel: nil,
@@ -46,8 +50,10 @@ struct ChatSessionSidebarModelTests {
modelProvider: nil,
model: nil,
contextTokens: nil,
label: label,
category: category,
pinned: pinned,
pinnedAt: pinnedAt,
archived: archived,
unread: unread,
agentStatus: agentStatus,
@@ -80,6 +86,49 @@ struct ChatSessionSidebarModelTests {
#expect(sections[1].title == "Recent")
}
@Test func `pinned sidebar sessions follow gateway pin chronology`() {
let sections = ChatSessionSidebarModel.sections(
sessions: [
self.entry(key: "pinned-old", updatedAt: 300, pinned: true, pinnedAt: 100),
self.entry(key: "pinned-new", updatedAt: 100, pinned: true, pinnedAt: 300),
self.entry(key: "recent", updatedAt: 400),
],
currentSessionKey: "recent",
query: "")
#expect(sections.map(\.id) == ["pinned", "recent"])
#expect(sections[0].nodes.map(\.session.key) == ["pinned-new", "pinned-old"])
#expect(sections[1].nodes.map(\.session.key) == ["recent"])
}
@Test func `sidebar sessions preserve deterministic gateway key ties`() {
let sections = ChatSessionSidebarModel.sections(
sessions: [
self.entry(key: "z-tie", updatedAt: 100),
self.entry(key: "a-tie", updatedAt: 100),
self.entry(key: "m-tie", updatedAt: 100),
],
currentSessionKey: "a-tie",
query: "")
#expect(sections.flatMap(\.nodes).map(\.session.key) == ["a-tie", "m-tie", "z-tie"])
}
@Test func `gateway groups preserve canonical session order`() {
let sections = ChatSessionSidebarModel.sections(
sessions: [
self.entry(key: "project-z", updatedAt: 100, category: "Projects"),
self.entry(key: "project-a", updatedAt: 100, category: "Projects"),
self.entry(key: "recent", updatedAt: 50),
],
currentSessionKey: "recent",
groups: [OpenClawChatSessionGroup(name: "Projects", position: 0)],
query: "")
#expect(sections.map(\.id) == ["group:Projects", "recent"])
#expect(sections[0].nodes.map(\.session.key) == ["project-a", "project-z"])
}
@Test func `single unpinned section carries no title`() {
let sections = ChatSessionSidebarModel.sections(
sessions: [self.entry(key: "a", updatedAt: 100)],
@@ -258,6 +307,52 @@ struct ChatSessionSidebarModelTests {
#expect(sections.flatMap(\.nodes).map(\.session.key) == ["agent:main:research"])
}
@Test(arguments: ["holiday", "KYOTO", "session-123", " HoLiDaY "])
func `sidebar search matches every canonical gateway session field`(_ query: String) {
let matching = self.entry(
key: "agent:main:roadmap",
displayName: "Planning",
label: "Summer holiday",
subject: "Kyoto itinerary",
sessionId: "session-123",
updatedAt: 200)
let other = self.entry(
key: "agent:main:other",
displayName: "Unrelated",
updatedAt: 100)
let sections = ChatSessionSidebarModel.sections(
sessions: [other, matching],
currentSessionKey: other.key,
query: query)
#expect(sections.flatMap(\.nodes).map(\.session.key) == [matching.key])
}
@Test(arguments: ["holiday", "KYOTO", "session-123"])
func `canonical search never reveals hidden or archived sidebar sessions`(_ query: String) {
let archived = self.entry(
key: "agent:main:archived",
label: "Summer holiday",
subject: "Kyoto itinerary",
sessionId: "session-123",
updatedAt: 300,
archived: true)
let onboarding = self.entry(
key: "agent:main:onboarding",
label: "Summer holiday",
subject: "Kyoto itinerary",
sessionId: "session-123",
updatedAt: 200)
let active = self.entry(key: "agent:main:active", updatedAt: 100)
let sections = ChatSessionSidebarModel.sections(
sessions: [archived, onboarding, active],
currentSessionKey: active.key,
query: query)
#expect(sections.isEmpty)
}
@Test func `session keys render as human names`() {
#expect(ChatSessionSidebarModel.displayName(forKey: "agent:main:main") == "main")
#expect(ChatSessionSidebarModel.displayName(forKey: "agent:ops:standup") == "standup (ops)")