mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-10 17:26:15 +00:00
* feat(chat-ui): native macOS chat window shell with sessions sidebar, context usage, and slash commands * feat(macos): host redesigned chat window shell and complete gateway chat transport * fix(chat-ui): gate slash panel on catalog support and register window shell shortcuts outside menus * fix(chat-ui): make sidebar selection tags optional and sync programmatic composer text into the focused NSTextView * fix(macos): track in-window session switches for chat window reuse * fix(chat-ui): re-bootstrap in place when deleting the active main session * docs(macos): describe the redesigned native chat window * fix(macos): scope bare global session RPCs to the routing default agent * style(chat-ui): restore blank line between rebased helpers * fix(macos): scope selected global session actions * fix(macos): refresh chat routing identity * fix(chat-ui): hide delete for main sessions * fix(macos): remove duplicate session patch path * fix(chat-ui): resolve sidebar session aliases * fix(chat-ui): isolate sidebar alias resolution * fix(chat-ui): reconcile active session aliases * fix(chat-ui): prefer selected session aliases * chore(i18n): refresh chat window inventory * fix(chat-ui): return sidebar filter result * refactor(chat-ui): split session actions
161 lines
6.1 KiB
Swift
161 lines
6.1 KiB
Swift
import Foundation
|
|
import Testing
|
|
@testable import OpenClawChatUI
|
|
|
|
@MainActor
|
|
struct ChatSessionSidebarModelTests {
|
|
private func entry(
|
|
key: String,
|
|
displayName: String? = nil,
|
|
updatedAt: Double? = nil,
|
|
pinned: Bool? = nil,
|
|
archived: Bool? = nil) -> OpenClawChatSessionEntry
|
|
{
|
|
OpenClawChatSessionEntry(
|
|
key: key,
|
|
kind: nil,
|
|
displayName: displayName,
|
|
surface: nil,
|
|
subject: nil,
|
|
room: nil,
|
|
space: nil,
|
|
updatedAt: updatedAt,
|
|
sessionId: nil,
|
|
systemSent: nil,
|
|
abortedLastRun: nil,
|
|
thinkingLevel: nil,
|
|
verboseLevel: nil,
|
|
inputTokens: nil,
|
|
outputTokens: nil,
|
|
totalTokens: nil,
|
|
modelProvider: nil,
|
|
model: nil,
|
|
contextTokens: nil,
|
|
pinned: pinned,
|
|
archived: archived)
|
|
}
|
|
|
|
@Test func `pinned sessions get their own section, rest sorted by recency`() {
|
|
let sections = ChatSessionSidebarModel.sections(
|
|
sessions: [
|
|
self.entry(key: "a", updatedAt: 100),
|
|
self.entry(key: "b", updatedAt: 300, pinned: true),
|
|
self.entry(key: "c", updatedAt: 200),
|
|
],
|
|
currentSessionKey: "a",
|
|
query: "")
|
|
|
|
#expect(sections.map(\.id) == ["pinned", "recent"])
|
|
#expect(sections[0].sessions.map(\.key) == ["b"])
|
|
#expect(sections[1].sessions.map(\.key) == ["c", "a"])
|
|
#expect(sections[1].title == "Recent")
|
|
}
|
|
|
|
@Test func `single unpinned section carries no title`() {
|
|
let sections = ChatSessionSidebarModel.sections(
|
|
sessions: [self.entry(key: "a", updatedAt: 100)],
|
|
currentSessionKey: "a",
|
|
query: "")
|
|
|
|
#expect(sections.count == 1)
|
|
#expect(sections[0].title == nil)
|
|
}
|
|
|
|
@Test func `hides onboarding and archived sessions, keeps the active one`() {
|
|
let sections = ChatSessionSidebarModel.sections(
|
|
sessions: [
|
|
self.entry(key: "agent:main:onboarding", updatedAt: 500),
|
|
self.entry(key: "gone", updatedAt: 400, archived: true),
|
|
self.entry(key: "main", updatedAt: 300),
|
|
],
|
|
currentSessionKey: "main",
|
|
query: "")
|
|
|
|
#expect(sections.flatMap(\.sessions).map(\.key) == ["main"])
|
|
}
|
|
|
|
@Test func `active session gets a placeholder row before lists load`() {
|
|
let sections = ChatSessionSidebarModel.sections(
|
|
sessions: [],
|
|
currentSessionKey: "agent:main:main",
|
|
query: "")
|
|
|
|
#expect(sections.flatMap(\.sessions).map(\.key) == ["agent:main:main"])
|
|
}
|
|
|
|
@Test func `main aliases select the resolved row without adding a placeholder`() {
|
|
let sessions = [self.entry(key: "agent:default:main", updatedAt: 100)]
|
|
let sections = ChatSessionSidebarModel.sections(
|
|
sessions: sessions,
|
|
currentSessionKey: "main",
|
|
mainSessionKey: "agent:default:main",
|
|
activeAgentID: "default",
|
|
query: "")
|
|
|
|
#expect(sections.flatMap(\.sessions).map(\.key) == ["agent:default:main"])
|
|
#expect(ChatSessionSidebarModel.selectedSessionKey(
|
|
sessions: sessions,
|
|
currentSessionKey: "main",
|
|
mainSessionKey: "agent:default:main",
|
|
activeAgentID: "default") == "agent:default:main")
|
|
}
|
|
|
|
@Test func `global aliases select their agent wrapped row`() {
|
|
let sessions = [
|
|
self.entry(key: "global", updatedAt: 200),
|
|
self.entry(key: "agent:ops:global", updatedAt: 100, archived: true),
|
|
]
|
|
let sections = ChatSessionSidebarModel.sections(
|
|
sessions: sessions,
|
|
currentSessionKey: "global",
|
|
mainSessionKey: "agent:main:main",
|
|
activeAgentID: "ops",
|
|
query: "")
|
|
|
|
#expect(ChatSessionSidebarModel.selectedSessionKey(
|
|
sessions: sessions,
|
|
currentSessionKey: "global",
|
|
mainSessionKey: "agent:main:main",
|
|
activeAgentID: "ops") == "agent:ops:global")
|
|
#expect(sections.flatMap(\.sessions).map(\.key) == ["agent:ops:global"])
|
|
}
|
|
|
|
@Test func `query filters on display name and key`() {
|
|
let sections = ChatSessionSidebarModel.sections(
|
|
sessions: [
|
|
self.entry(key: "agent:main:research", displayName: "Deep Research", updatedAt: 200),
|
|
self.entry(key: "agent:main:main", updatedAt: 100),
|
|
],
|
|
currentSessionKey: "agent:main:main",
|
|
query: "research")
|
|
|
|
#expect(sections.flatMap(\.sessions).map(\.key) == ["agent:main:research"])
|
|
}
|
|
|
|
@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)")
|
|
#expect(ChatSessionSidebarModel.displayName(forKey: "global") == "global")
|
|
}
|
|
|
|
@Test func `display name prefers explicit names over key prettifying`() {
|
|
let named = self.entry(key: "agent:main:x", displayName: " Weekly Sync ")
|
|
#expect(ChatSessionSidebarModel.displayName(for: named) == "Weekly Sync")
|
|
|
|
let unnamed = self.entry(key: "agent:main:x")
|
|
#expect(ChatSessionSidebarModel.displayName(for: unnamed) == "x")
|
|
}
|
|
|
|
@Test func `delete excludes main aliases and allows ordinary or selected global sessions`() {
|
|
let mainKey = "agent:default:main"
|
|
|
|
#expect(!ChatSessionSidebarModel.canDeleteSession(key: "main", mainSessionKey: mainKey))
|
|
#expect(!ChatSessionSidebarModel.canDeleteSession(key: "GLOBAL", mainSessionKey: mainKey))
|
|
#expect(!ChatSessionSidebarModel.canDeleteSession(key: mainKey, mainSessionKey: mainKey))
|
|
#expect(ChatSessionSidebarModel.canDeleteSession(key: "scratch", mainSessionKey: mainKey))
|
|
#expect(ChatSessionSidebarModel.canDeleteSession(
|
|
key: "agent:other:global",
|
|
mainSessionKey: mainKey))
|
|
}
|
|
}
|