feat: paginate, sync, and display branched sessions (#563)

* mobile: 以 Desktop Sessions 作为聊天历史源

* mobile: 修复快速连接崩溃与无标题会话隐藏

* mobile: 对齐 Desktop 标题并恢复完整历史

* mobile: 分离 Desktop 存储 ID 与运行 Session ID

* mobile: 将新对话持久化为 Desktop Session

* mobile: 渲染 Desktop 本地媒体

* feat: 分页加载并同步当前会话

* fix: 显示分支会话名称与层级

* fix: align PR with upstream app contract

* fix: preserve upstream session RPC lifecycle
This commit is contained in:
Genesi599 authored and GitHub committed 2026-07-12 12:53:23 +03:00
1 parent 306861a222
commit 3052db7d65
8 files changed
+405 -79

No files matched your search

@@ -0,0 +1,45 @@
package com.m57.hermescontrol.data.model
import org.junit.Assert.assertEquals
import org.junit.Test
class SessionTreeTest {
@Test
fun nestsBranchesAndUsesDesktopFallbackNames() {
val sessions =
listOf(
SessionInfo(id = "unrelated", title = "Recent root"),
SessionInfo(id = "branch-b", parent_session_id = "parent"),
SessionInfo(id = "parent", title = "Original"),
SessionInfo(id = "branch-a", parent_session_id = "parent", preview = "copied prompt"),
SessionInfo(id = "nested", parent_session_id = "branch-a", title = "Named nested branch"),
)
val tree = flattenSessionTree(sessions)
assertEquals(listOf("unrelated", "parent", "branch-b", "branch-a", "nested"), tree.map { it.session.id })
assertEquals(listOf(0, 0, 1, 1, 2), tree.map { it.depth })
assertEquals(
listOf("Recent root", "Original", "branch 1", "branch 2", "Named nested branch"),
tree.map {
it.displayTitle
},
)
assertEquals(listOf(null, null, "├─", "└─", "└─"), tree.map { it.branchStem })
}
@Test
fun keepsMissingParentsAndCyclesVisible() {
val sessions =
listOf(
SessionInfo(id = "orphan", parent_session_id = "missing", preview = "orphan preview"),
SessionInfo(id = "a", parent_session_id = "b"),
SessionInfo(id = "b", parent_session_id = "a"),
)
val tree = flattenSessionTree(sessions)
assertEquals(setOf("orphan", "a", "b"), tree.map { it.session.id }.toSet())
assertEquals("orphan preview", tree.first { it.session.id == "orphan" }.displayTitle)
}
}
@@ -141,7 +141,8 @@ class HermesApiServiceMockWebServerTest {
"created_at": "2026-06-20T14:30:00Z",
"message_count": 7,
"status": "idle",
"source": "web"
"source": "web",
"parent_session_id": "abc-123"
}
],
"total": 2,
@@ -173,6 +174,7 @@ class HermesApiServiceMockWebServerTest {
assertEquals(7, second.message_count)
assertEquals("idle", second.status)
assertEquals("web", second.source)
assertEquals("abc-123", second.parent_session_id)
}
@Test
@@ -254,6 +256,24 @@ class HermesApiServiceMockWebServerTest {
assertEquals("tool_execution", messages[2].type)
}
@Test
fun getSessionMessages_sendsPaginationQuery() =
runBlocking {
mockServer.enqueue(
MockResponse()
.setResponseCode(200)
.setBody("{\"messages\": []}"),
)
val response = api.getSessionMessages("desktop/session", limit = 150, offset = 300)
assertTrue(response.isSuccessful)
assertEquals(
"/api/sessions/desktop/session/messages?limit=150&offset=300",
mockServer.takeRequest().path,
)
}
@Test
fun getSessionMessages_withNullRole_doesNotCrash() =
runBlocking {