mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-10 21:46:08 +00:00
Gateway (additive, no protocol version bump): SessionEntry gains lastReadAt/markedUnreadAt/lastActivityAt; session rows expose a derived unread flag (explicit mark, or last read before latest activity; never-read sessions stay read so upgrades do not light up). lastActivityAt is stamped in the canonical post-run store update - user, channel, and cron runs count as activity; heartbeat, internal-event, and preserved-state runs do not. sessions.patch gains unread; sessions.create gains fork (transcript fork from parentSessionKey under the parent lifecycle lock, refusing active, concurrently-changed, and oversized parents, cross-agent aware). Web sidebar: Pinned/custom-group/Ungrouped sections, unread dots, kebab and right-click context menu (pin, mark unread/read, rename, fork, move to group, archive, delete guarded for agent main sessions and active runs), mark-read on view with loop-safe re-acknowledgement and failure retry; sessions page gets unread + fork actions and shared custom-group helpers. iOS Command Center: grouped sections, unread/pin indicators, Show Archived gated on per-entry state, full context menu with rename/new-group alerts and delete confirmation, current-session preview guarantee, read-episode re-acknowledgement; new patch/delete/fork transport calls; Swift protocol models regenerated. Android SessionsScreen: grouped headers, unread/pin indicators, Archived filter gated on per-entry state, long-press menu with the full control set, agent-scoped forks, explicit label/category clears from session events, main-session fallback when archiving/deleting the open chat, read-episode re-acknowledgement with failure retry. Closes #100739
95 lines
3.3 KiB
Swift
95 lines
3.3 KiB
Swift
import OpenClawChatUI
|
|
import Testing
|
|
@testable import OpenClaw
|
|
|
|
struct CommandSessionGroupingTests {
|
|
@Test func `groups pinned categories and ungrouped in display order`() {
|
|
let sections = CommandSessionGrouping.sections(from: [
|
|
self.entry("ungrouped", activity: 2),
|
|
self.entry("beta", category: "Beta", activity: 3),
|
|
self.entry("alpha-old", category: "Alpha", activity: 1),
|
|
self.entry("pinned", category: "Beta", pinned: true, activity: 4),
|
|
self.entry("alpha-new", category: "Alpha", activity: 5),
|
|
])
|
|
|
|
#expect(sections.map(\.id) == [
|
|
.pinned,
|
|
.category("Alpha"),
|
|
.category("Beta"),
|
|
.ungrouped,
|
|
])
|
|
#expect(sections[0].entries.map(\.key) == ["pinned"])
|
|
#expect(sections[1].entries.map(\.key) == ["alpha-new", "alpha-old"])
|
|
#expect(sections[3].showsHeader)
|
|
}
|
|
|
|
@Test func `hides ungrouped header without category sections`() {
|
|
let sections = CommandSessionGrouping.sections(from: [self.entry("plain", activity: 1)])
|
|
|
|
#expect(sections.count == 1)
|
|
#expect(sections[0].id == .ungrouped)
|
|
#expect(!sections[0].showsHeader)
|
|
}
|
|
|
|
@Test func `preview puts pinned sessions before recent activity`() {
|
|
let entries = CommandSessionGrouping.previewOrder([
|
|
self.entry("recent", activity: 20),
|
|
self.entry("pinned-old", pinned: true, activity: 1),
|
|
self.entry("older", activity: 10),
|
|
])
|
|
|
|
#expect(entries.map(\.key) == ["pinned-old", "recent", "older"])
|
|
}
|
|
|
|
@Test func `preview selection keeps the open chat visible past the cap`() {
|
|
let entries = [
|
|
self.entry("a", activity: 40),
|
|
self.entry("b", activity: 30),
|
|
self.entry("c", activity: 20),
|
|
self.entry("current", activity: 10),
|
|
]
|
|
|
|
let selection = CommandSessionGrouping.previewSelection(entries, currentKey: "current")
|
|
#expect(selection.map(\.key) == ["current", "a", "b"])
|
|
|
|
// Natural order wins when the current session already fits the cap.
|
|
let natural = CommandSessionGrouping.previewSelection(entries, currentKey: "a")
|
|
#expect(natural.map(\.key) == ["a", "b", "c"])
|
|
|
|
// Unknown or empty keys fall back to the plain capped ordering.
|
|
let fallback = CommandSessionGrouping.previewSelection(entries, currentKey: "")
|
|
#expect(fallback.map(\.key) == ["a", "b", "c"])
|
|
}
|
|
|
|
private func entry(
|
|
_ key: String,
|
|
category: String? = nil,
|
|
pinned: Bool = false,
|
|
activity: Double) -> OpenClawChatSessionEntry
|
|
{
|
|
OpenClawChatSessionEntry(
|
|
key: key,
|
|
kind: nil,
|
|
displayName: nil,
|
|
surface: nil,
|
|
subject: nil,
|
|
room: nil,
|
|
space: nil,
|
|
updatedAt: nil,
|
|
sessionId: nil,
|
|
systemSent: nil,
|
|
abortedLastRun: nil,
|
|
thinkingLevel: nil,
|
|
verboseLevel: nil,
|
|
inputTokens: nil,
|
|
outputTokens: nil,
|
|
totalTokens: nil,
|
|
modelProvider: nil,
|
|
model: nil,
|
|
contextTokens: nil,
|
|
category: category,
|
|
pinned: pinned,
|
|
lastActivityAt: activity)
|
|
}
|
|
}
|