feat: sync device codex projects
This commit is contained in:
@@ -10,6 +10,7 @@ data class AppStatePayload(
|
||||
val tasks: List<TaskItem> = emptyList(),
|
||||
val workers: List<WorkerNode> = emptyList(),
|
||||
val deviceBindings: List<DeviceBinding> = emptyList(),
|
||||
val deviceProjects: List<DeviceProject> = emptyList(),
|
||||
val approvals: List<ApprovalRequest> = emptyList(),
|
||||
val events: List<BossEvent> = emptyList(),
|
||||
)
|
||||
@@ -123,6 +124,23 @@ data class DeviceBinding(
|
||||
val updatedAt: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DeviceProject(
|
||||
val id: String,
|
||||
val workerId: String,
|
||||
val source: String,
|
||||
val workspaceRoot: String,
|
||||
val workspaceLabel: String,
|
||||
val projectName: String,
|
||||
val status: String,
|
||||
val primaryThreadId: String? = null,
|
||||
val primaryThreadTitle: String = "",
|
||||
val recentThreadTitles: List<String> = emptyList(),
|
||||
val recentThreadCount: Int = 0,
|
||||
val pinnedThreadIds: List<String> = emptyList(),
|
||||
val updatedAt: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DeviceBindingLaunchPayload(
|
||||
val binding: DeviceBinding,
|
||||
|
||||
@@ -74,6 +74,7 @@ import java.time.format.DateTimeFormatter
|
||||
import site.hyzq.bossandroid.model.ApprovalRequest
|
||||
import site.hyzq.bossandroid.model.BossEvent
|
||||
import site.hyzq.bossandroid.model.DeviceBinding
|
||||
import site.hyzq.bossandroid.model.DeviceProject
|
||||
import site.hyzq.bossandroid.model.Message
|
||||
import site.hyzq.bossandroid.model.Session
|
||||
import site.hyzq.bossandroid.model.TaskItem
|
||||
@@ -576,6 +577,7 @@ private fun OverviewTab(
|
||||
onSelectSession: (String) -> Unit,
|
||||
onSelectWorker: (String?) -> Unit,
|
||||
) {
|
||||
val selectedWorker = uiState.workers.firstOrNull { it.id == uiState.selectedWorkerId }
|
||||
val messagesBySession = remember(uiState.messages) {
|
||||
uiState.messages.groupBy { it.sessionId }
|
||||
}
|
||||
@@ -585,11 +587,44 @@ private fun OverviewTab(
|
||||
val workerById = remember(uiState.workers) {
|
||||
uiState.workers.associateBy { it.id }
|
||||
}
|
||||
val activeTasks = remember(uiState.tasks) {
|
||||
uiState.tasks.filter { it.status in listOf("planning", "queued", "assigned", "running", "blocked", "paused", "waiting_approval") }
|
||||
val selectedWorkerSessionIds = remember(uiState.selectedWorkerId, uiState.sessions, uiState.tasks) {
|
||||
if (uiState.selectedWorkerId == null) {
|
||||
uiState.sessions.map { it.id }.toSet()
|
||||
} else {
|
||||
mutableSetOf<String>().apply {
|
||||
uiState.sessions
|
||||
.filter { session -> session.activeWorkerId == uiState.selectedWorkerId }
|
||||
.forEach { add(it.id) }
|
||||
uiState.tasks
|
||||
.filter {
|
||||
it.assignedWorkerId == uiState.selectedWorkerId ||
|
||||
it.preferredWorkerId == uiState.selectedWorkerId
|
||||
}
|
||||
.forEach { add(it.sessionId) }
|
||||
}
|
||||
}
|
||||
}
|
||||
val recentMessages = remember(uiState.messages) {
|
||||
uiState.messages.sortedByDescending { it.createdAt }.take(12)
|
||||
val visibleSessions = remember(uiState.sessions, selectedWorkerSessionIds) {
|
||||
uiState.sessions.filter { it.id in selectedWorkerSessionIds }
|
||||
}
|
||||
val activeTasks = remember(uiState.tasks, uiState.selectedWorkerId) {
|
||||
uiState.tasks.filter {
|
||||
it.status in listOf("planning", "queued", "assigned", "running", "blocked", "paused", "waiting_approval") &&
|
||||
(uiState.selectedWorkerId == null ||
|
||||
it.assignedWorkerId == uiState.selectedWorkerId ||
|
||||
it.preferredWorkerId == uiState.selectedWorkerId)
|
||||
}
|
||||
}
|
||||
val recentMessages = remember(uiState.messages, selectedWorkerSessionIds) {
|
||||
uiState.messages
|
||||
.filter { it.sessionId in selectedWorkerSessionIds }
|
||||
.sortedByDescending { it.createdAt }
|
||||
.take(12)
|
||||
}
|
||||
val visibleDeviceProjects = remember(uiState.deviceProjects, uiState.selectedWorkerId) {
|
||||
uiState.deviceProjects.filter { project ->
|
||||
uiState.selectedWorkerId == null || project.workerId == uiState.selectedWorkerId
|
||||
}
|
||||
}
|
||||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
@@ -613,14 +648,34 @@ private fun OverviewTab(
|
||||
}
|
||||
}
|
||||
|
||||
SectionHeading("所有项目")
|
||||
if (uiState.sessions.isEmpty()) {
|
||||
SectionHeading(if (selectedWorker != null) "${selectedWorker.name} 的 Codex 项目" else "设备 Codex 项目")
|
||||
if (visibleDeviceProjects.isEmpty()) {
|
||||
EmptyStateCard(
|
||||
title = "还没有项目",
|
||||
body = "你可以在手机上创建,也可以在电脑入口创建。Boss 会自动把它们同步到这里。",
|
||||
title = "当前还没有同步到 Codex 项目",
|
||||
body = if (selectedWorker != null) {
|
||||
"这台设备还没把本机 Codex 项目索引上报到 Boss,等下一次心跳后会自动刷新。"
|
||||
} else {
|
||||
"选中一台设备后,这里会显示该设备当前打开或最近活跃的 Codex 项目。"
|
||||
},
|
||||
)
|
||||
} else {
|
||||
uiState.sessions.forEach { session ->
|
||||
visibleDeviceProjects.forEach { project ->
|
||||
DeviceProjectCard(project = project)
|
||||
}
|
||||
}
|
||||
|
||||
SectionHeading(if (selectedWorker != null) "该设备关联会话" else "所有项目")
|
||||
if (visibleSessions.isEmpty()) {
|
||||
EmptyStateCard(
|
||||
title = if (selectedWorker != null) "这台设备还没有关联会话" else "还没有项目",
|
||||
body = if (selectedWorker != null) {
|
||||
"你可以先切到这台设备发起任务,Boss 会把相关会话自动归到它下面。"
|
||||
} else {
|
||||
"你可以在手机上创建,也可以在电脑入口创建。Boss 会自动把它们同步到这里。"
|
||||
},
|
||||
)
|
||||
} else {
|
||||
visibleSessions.forEach { session ->
|
||||
val recentMessage = messagesBySession[session.id]?.maxByOrNull { it.createdAt }
|
||||
val sessionTasks = tasksBySession[session.id].orEmpty()
|
||||
val sessionWorkerNames = sessionTasks
|
||||
@@ -1360,6 +1415,72 @@ private fun ProjectOverviewCard(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DeviceProjectCard(
|
||||
project: DeviceProject,
|
||||
) {
|
||||
OutlinedCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.outlinedCardColors(
|
||||
containerColor = if (project.status == "active") {
|
||||
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.35f)
|
||||
} else {
|
||||
MaterialTheme.colorScheme.surface
|
||||
},
|
||||
),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(project.projectName, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold)
|
||||
Text(
|
||||
project.workspaceRoot,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
StatusChip(
|
||||
label = if (project.status == "active") "当前活跃" else "最近活跃",
|
||||
tone = if (project.status == "active") "running" else "pending",
|
||||
)
|
||||
}
|
||||
|
||||
if (project.primaryThreadTitle.isNotBlank()) {
|
||||
Text("主对话:${project.primaryThreadTitle}")
|
||||
}
|
||||
if (project.recentThreadTitles.isNotEmpty()) {
|
||||
Text(
|
||||
"最近对话:${project.recentThreadTitles.joinToString(" · ")}",
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
AssistChip(onClick = {}, label = { Text("${project.recentThreadCount} 个对话") })
|
||||
if (project.pinnedThreadIds.isNotEmpty()) {
|
||||
AssistChip(onClick = {}, label = { Text("${project.pinnedThreadIds.size} 个置顶") })
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
"最近同步 ${formatRelative(project.updatedAt)}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GlobalMessageCard(
|
||||
message: Message,
|
||||
|
||||
@@ -19,6 +19,7 @@ import site.hyzq.bossandroid.model.AppStatePayload
|
||||
import site.hyzq.bossandroid.model.ApprovalRequest
|
||||
import site.hyzq.bossandroid.model.BossEvent
|
||||
import site.hyzq.bossandroid.model.DeviceBinding
|
||||
import site.hyzq.bossandroid.model.DeviceProject
|
||||
import site.hyzq.bossandroid.model.HealthPayload
|
||||
import site.hyzq.bossandroid.model.Message
|
||||
import site.hyzq.bossandroid.model.Session
|
||||
@@ -63,6 +64,7 @@ data class BossUiState(
|
||||
val approvals: List<ApprovalRequest> = emptyList(),
|
||||
val workers: List<WorkerNode> = emptyList(),
|
||||
val deviceBindings: List<DeviceBinding> = emptyList(),
|
||||
val deviceProjects: List<DeviceProject> = emptyList(),
|
||||
val events: List<BossEvent> = emptyList(),
|
||||
val selectedSessionId: String? = null,
|
||||
val selectedWorkerId: String? = null,
|
||||
@@ -361,6 +363,10 @@ class BossViewModel(
|
||||
val approvals = snapshot.approvals.sortedByDescending { it.updatedAt }
|
||||
val workers = snapshot.workers.sortedBy { it.name.lowercase() }
|
||||
val deviceBindings = snapshot.deviceBindings.sortedByDescending { it.updatedAt }
|
||||
val deviceProjects = snapshot.deviceProjects.sortedWith(
|
||||
compareByDescending<DeviceProject> { it.status == "active" }
|
||||
.thenByDescending { it.updatedAt },
|
||||
)
|
||||
val events = snapshot.events.sortedByDescending { it.timestamp }
|
||||
|
||||
val selectedSessionId = preferredSessionId
|
||||
@@ -383,6 +389,7 @@ class BossViewModel(
|
||||
approvals = approvals,
|
||||
workers = workers,
|
||||
deviceBindings = deviceBindings,
|
||||
deviceProjects = deviceProjects,
|
||||
events = events,
|
||||
health = HealthPayload(
|
||||
status = "ok",
|
||||
|
||||
Reference in New Issue
Block a user