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

@@ -76,6 +76,8 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import com.m57.hermescontrol.ChatScreen
import com.m57.hermescontrol.NavigationController
import com.m57.hermescontrol.R
import com.m57.hermescontrol.data.model.SessionInfo
import com.m57.hermescontrol.data.model.flattenSessionTree
import com.m57.hermescontrol.theme.LocalHermesStatusColors
import com.m57.hermescontrol.theme.LocalSpacing
import com.m57.hermescontrol.ui.common.EmptyState
@@ -170,11 +172,17 @@ fun SessionsScreen(
var query by remember { mutableStateOf("") }
var pruneDays by remember { mutableStateOf("7") }
val sessionTree = remember(state.sessions) { flattenSessionTree(state.sessions) }
val filteredSessions =
remember(query, state.sessions) {
state.sessions.filter { session ->
session.title?.contains(query, ignoreCase = true) == true ||
session.status?.contains(query, ignoreCase = true) == true
remember(query, sessionTree) {
if (query.isBlank()) {
sessionTree
} else {
sessionTree.filter { item ->
item.displayTitle.contains(query, ignoreCase = true) ||
item.session.preview?.contains(query, ignoreCase = true) == true ||
item.session.status?.contains(query, ignoreCase = true) == true
}
}
}
@@ -430,9 +438,13 @@ fun SessionsScreen(
contentPadding = listContentPadding,
verticalArrangement = listItemSpacing,
) {
items(filteredSessions, key = { it.id }) { session ->
items(filteredSessions, key = { it.session.id }) { item ->
val session = item.session
SessionCard(
session = session,
displayTitle = item.displayTitle,
depth = item.depth,
branchStem = item.branchStem,
query = query,
isSelecting = state.isSelecting,
isSelected = session.id in state.selectedIds,
@@ -589,7 +601,10 @@ fun SessionsScreen(
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun SessionCard(
session: com.m57.hermescontrol.data.model.SessionInfo,
session: SessionInfo,
displayTitle: String,
depth: Int,
branchStem: String?,
query: String,
isSelecting: Boolean,
isSelected: Boolean,
@@ -603,7 +618,6 @@ private fun SessionCard(
) {
val spacing = LocalSpacing.current
val statusColors = LocalHermesStatusColors.current
val displayTitle = session.title?.takeIf { it.isNotBlank() } ?: stringResource(R.string.history_untitled)
val isActive = session.status?.lowercase() == "active" || session.status?.lowercase() == "streaming"
val srcIcon = sourceIcon(session.source)
@@ -611,6 +625,7 @@ private fun SessionCard(
modifier =
Modifier
.fillMaxWidth()
.padding(start = (depth * 16).dp)
.testTag("session_card_${session.id}")
.combinedClickable(
onClick = onCardClick,
@@ -643,6 +658,16 @@ private fun SessionCard(
Spacer(modifier = Modifier.width(spacing.sm))
}
// Branch tree stem
if (branchStem != null && !isSelecting) {
Text(
text = branchStem,
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.outline,
)
Spacer(modifier = Modifier.width(spacing.sm))
}
// Source icon
if (srcIcon != null && !isSelecting) {
Icon(