feat: sync device codex projects

This commit is contained in:
Codex
2026-03-25 03:42:36 +08:00
parent fd1e0735c7
commit ee9ade6bd3
9 changed files with 497 additions and 14 deletions

View File

@@ -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,

View File

@@ -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,

View File

@@ -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",