polish issue detail screenshot feedback

This commit is contained in:
Xisheng-Zhao
2026-05-17 17:51:37 +08:00
parent 0e2035a480
commit bf12771908
2 changed files with 183 additions and 166 deletions
@@ -67,6 +67,7 @@ import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material.icons.outlined.Email import androidx.compose.material.icons.outlined.Email
import androidx.compose.material.icons.outlined.Flag import androidx.compose.material.icons.outlined.Flag
import androidx.compose.material.icons.outlined.Folder import androidx.compose.material.icons.outlined.Folder
import androidx.compose.material.icons.outlined.Image
import androidx.compose.material.icons.outlined.Key import androidx.compose.material.icons.outlined.Key
import androidx.compose.material.icons.outlined.KeyboardArrowDown import androidx.compose.material.icons.outlined.KeyboardArrowDown
import androidx.compose.material.icons.outlined.KeyboardArrowRight import androidx.compose.material.icons.outlined.KeyboardArrowRight
@@ -7565,8 +7566,6 @@ private fun IssueDetailHeroCard(
zh: Boolean, zh: Boolean,
onStatusChange: (String) -> Unit, onStatusChange: (String) -> Unit,
onPriorityChange: (String) -> Unit, onPriorityChange: (String) -> Unit,
onEdit: () -> Unit,
onCopyLink: () -> Unit,
) { ) {
Surface( Surface(
modifier = Modifier modifier = Modifier
@@ -7588,20 +7587,6 @@ private fun IssueDetailHeroCard(
horizontalArrangement = Arrangement.spacedBy(10.dp), horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Box(
modifier = Modifier
.size(38.dp)
.clip(RoundedCornerShape(12.dp))
.background(MulticaColors.AccentSoft.copy(alpha = 0.38f)),
contentAlignment = Alignment.Center,
) {
Text(
text = issue.identifier.takeLastWhile { it.isDigit() }.take(2).ifBlank { issue.identifier.take(2) },
style = MaterialTheme.typography.labelLarge.copy(fontSize = 13.sp, lineHeight = 16.sp, fontWeight = FontWeight.Bold),
color = MulticaColors.Accent,
maxLines = 1,
)
}
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) { Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
Text( Text(
text = issue.identifier, text = issue.identifier,
@@ -7614,9 +7599,9 @@ private fun IssueDetailHeroCard(
Text( Text(
text = issue.title, text = issue.title,
style = MaterialTheme.typography.titleLarge.copy( style = MaterialTheme.typography.titleLarge.copy(
fontWeight = FontWeight.Bold, fontWeight = FontWeight.SemiBold,
fontSize = 22.sp, fontSize = 20.sp,
lineHeight = 27.sp, lineHeight = 24.sp,
), ),
color = MulticaColors.Text, color = MulticaColors.Text,
maxLines = 3, maxLines = 3,
@@ -7624,18 +7609,6 @@ private fun IssueDetailHeroCard(
) )
} }
} }
MulticaIconPillButton(
icon = Icons.Outlined.ContentCopy,
contentDescription = "Issue Detail Hero Copy Link",
onClick = onCopyLink,
tone = MulticaButtonTone.Ghost,
)
MulticaIconPillButton(
icon = Icons.Outlined.Edit,
contentDescription = "Issue Detail Hero Edit",
onClick = onEdit,
tone = MulticaButtonTone.Ghost,
)
} }
LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth()) { LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth()) {
item { item {
@@ -7669,58 +7642,125 @@ private fun IssueDetailCompactMetadata(
) { ) {
val labelSummary = issueLabels.joinToString(", ") { it.name } val labelSummary = issueLabels.joinToString(", ") { it.name }
.ifBlank { if (zh) "无 Labels" else "No labels" } .ifBlank { if (zh) "无 Labels" else "No labels" }
CompactInfoPanel( val dueValue = issue.dueDate?.takeIf { it.isNotBlank() }?.let { shortDate(it) } ?: "-"
val createdValue = issue.createdAt.takeIf { it.isNotBlank() }?.let { shortDate(it) } ?: "-"
val updatedValue = issue.updatedAt.takeIf { it.isNotBlank() }?.let { shortDate(it) } ?: "-"
Surface(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(top = 2.dp, bottom = 10.dp) .padding(top = 2.dp, bottom = 8.dp)
.semantics(mergeDescendants = false) { contentDescription = "Issue Detail Compact Metadata" }, .semantics(mergeDescendants = false) { contentDescription = "Issue Detail Compact Metadata" },
shape = RoundedCornerShape(12.dp),
color = MulticaColors.Surface.copy(alpha = 0.82f),
tonalElevation = 0.dp,
shadowElevation = 0.dp,
border = androidx.compose.foundation.BorderStroke(0.75.dp, MulticaColors.Border.copy(alpha = 0.42f)),
) { ) {
Row(horizontalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier.fillMaxWidth()) { Column(
CompactMetaText( modifier = Modifier.padding(horizontal = 13.dp, vertical = 11.dp),
label = if (zh) "负责人" else "Assignee", verticalArrangement = Arrangement.spacedBy(9.dp),
value = assignee.ifBlank { if (zh) "未分配" else "Unassigned" }, ) {
modifier = Modifier.weight(1f), Row(horizontalArrangement = Arrangement.spacedBy(10.dp), modifier = Modifier.fillMaxWidth()) {
contentDescription = "Issue Detail Assignee Metadata Row", IssueDetailTinyProperty(
) label = if (zh) "负责人" else "Assignee",
CompactMetaText( value = assignee.ifBlank { if (zh) "未分配" else "Unassigned" },
label = if (zh) "项目" else "Project", icon = Icons.Outlined.Person,
value = project, modifier = Modifier.weight(1f),
modifier = Modifier.weight(1f), contentDescription = "Issue Detail Assignee Metadata Row",
contentDescription = "Issue Detail Project Metadata Row", )
) IssueDetailTinyProperty(
label = if (zh) "项目" else "Project",
value = project,
icon = Icons.Outlined.Folder,
modifier = Modifier.weight(1f),
contentDescription = "Issue Detail Project Metadata Row",
)
}
Row(horizontalArrangement = Arrangement.spacedBy(10.dp), modifier = Modifier.fillMaxWidth()) {
IssueDetailTinyProperty(
label = if (zh) "优先级" else "Priority",
value = Models.priorityLabel(issue.priority, zh),
icon = Icons.Outlined.Flag,
modifier = Modifier.weight(1f),
contentDescription = "Issue Detail Priority Due Date Metadata Row",
)
IssueDetailTinyProperty(
label = if (zh) "到期" else "Due",
value = dueValue,
icon = Icons.Outlined.CalendarMonth,
modifier = Modifier.weight(1f),
contentDescription = "Issue Detail Due Date Metadata Row",
)
}
Row(horizontalArrangement = Arrangement.spacedBy(10.dp), modifier = Modifier.fillMaxWidth()) {
IssueDetailTinyProperty(
label = if (zh) "Labels" else "Labels",
value = labelSummary,
icon = Icons.Outlined.Label,
modifier = Modifier.weight(1f),
contentDescription = "Issue Detail Labels Metadata Row",
)
IssueDetailTinyProperty(
label = if (zh) "创建" else "Created",
value = createdValue,
icon = Icons.Outlined.CalendarMonth,
modifier = Modifier.weight(1f),
contentDescription = "Issue Detail Created Metadata Row",
)
}
Row(horizontalArrangement = Arrangement.spacedBy(10.dp), modifier = Modifier.fillMaxWidth()) {
IssueDetailTinyProperty(
label = if (zh) "更新" else "Updated",
value = updatedValue,
icon = Icons.Outlined.CalendarMonth,
modifier = Modifier.weight(1f),
contentDescription = "Issue Detail Updated Metadata Row",
)
Spacer(modifier = Modifier.weight(1f))
}
} }
Row(horizontalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier.fillMaxWidth()) { }
CompactMetaText( }
label = if (zh) "优先级" else "Priority",
value = Models.priorityLabel(issue.priority, zh), @Composable
modifier = Modifier.weight(1f), private fun IssueDetailTinyProperty(
contentDescription = "Issue Detail Priority Due Date Metadata Row", label: String,
value: String,
icon: ImageVector,
modifier: Modifier = Modifier,
contentDescription: String? = null,
) {
Row(
modifier = modifier.then(
if (contentDescription == null) Modifier else Modifier.semantics(mergeDescendants = true) {
this.contentDescription = contentDescription
}
),
horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalAlignment = Alignment.Top,
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = MulticaColors.TextTertiary,
modifier = Modifier
.padding(top = 1.dp)
.size(13.dp),
)
Column(verticalArrangement = Arrangement.spacedBy(1.dp), modifier = Modifier.weight(1f)) {
Text(
text = label,
style = MaterialTheme.typography.labelSmall.copy(fontSize = 10.sp, lineHeight = 12.sp, fontWeight = FontWeight.SemiBold),
color = MulticaColors.TextTertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
) )
CompactMetaText( Text(
label = if (zh) "到期" else "Due", text = value,
value = issue.dueDate?.takeIf { it.isNotBlank() }?.let { shortDate(it) } ?: "-", style = MaterialTheme.typography.bodySmall.copy(fontSize = 12.sp, lineHeight = 15.sp, fontWeight = FontWeight.Medium),
modifier = Modifier.weight(1f), color = MulticaColors.Text,
contentDescription = "Issue Detail Priority Due Date Metadata Row", maxLines = 1,
) overflow = TextOverflow.Ellipsis,
}
Row(horizontalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier.fillMaxWidth()) {
CompactMetaText(
label = if (zh) "Labels" else "Labels",
value = labelSummary,
modifier = Modifier.weight(1f),
contentDescription = "Issue Detail Labels Metadata Row",
)
}
Row(horizontalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier.fillMaxWidth()) {
CompactMetaText(
label = if (zh) "创建" else "Created",
value = issue.createdAt.takeIf { it.isNotBlank() }?.let { shortDate(it) } ?: "-",
modifier = Modifier.weight(1f),
)
CompactMetaText(
label = if (zh) "更新" else "Updated",
value = issue.updatedAt.takeIf { it.isNotBlank() }?.let { shortDate(it) } ?: "-",
modifier = Modifier.weight(1f),
) )
} }
} }
@@ -8508,6 +8548,7 @@ private fun IssueDetailProgressCard(
subtitle: String?, subtitle: String?,
icon: ImageVector, icon: ImageVector,
tone: MulticaButtonTone, tone: MulticaButtonTone,
contentDescription: String? = null,
onClick: (() -> Unit)? = null, onClick: (() -> Unit)? = null,
) { ) {
val tint = when (tone) { val tint = when (tone) {
@@ -8523,7 +8564,16 @@ private fun IssueDetailProgressCard(
.fillMaxWidth() .fillMaxWidth()
.then(clickModifier) .then(clickModifier)
.clip(shape) .clip(shape)
.border(0.75.dp, MulticaColors.Border.copy(alpha = 0.52f), shape), .border(0.75.dp, MulticaColors.Border.copy(alpha = 0.52f), shape)
.semantics(mergeDescendants = true) {
this.contentDescription = contentDescription ?: "Issue Detail Progress Card"
if (onClick != null) {
semanticsOnClick(label = title) {
onClick()
true
}
}
},
shape = shape, shape = shape,
color = MulticaColors.Surface.copy(alpha = 0.72f), color = MulticaColors.Surface.copy(alpha = 0.72f),
) { ) {
@@ -9724,8 +9774,6 @@ private fun PilotIssueDetail(
zh = zh, zh = zh,
onStatusChange = { updateIssueStatusFromDetail(it) }, onStatusChange = { updateIssueStatusFromDetail(it) },
onPriorityChange = { updateIssuePriorityFromDetail(it) }, onPriorityChange = { updateIssuePriorityFromDetail(it) },
onEdit = { onEditIssue(data.issue) },
onCopyLink = { copyIssueLink() },
) )
} }
item { item {
@@ -9760,73 +9808,30 @@ private fun PilotIssueDetail(
MarkdownBlock(data.issue.description) MarkdownBlock(data.issue.description)
} }
} }
item { if (data.activeTasks.isNotEmpty() || data.runs.isNotEmpty()) {
SectionHeader(if (zh) "Agent 最新进展" else "Agent Latest Progress") item {
} val task = data.activeTasks.firstOrNull()
item { val run = data.runs.firstOrNull()
IssueDetailWebActionRow( IssueDetailProgressCard(
eyebrow = if (zh) "Agent Live" else "Agent live", title = if (task != null) {
title = if (data.activeTasks.isNotEmpty()) { if (zh) "活跃 Agent 任务" else "Active agent task"
if (zh) "正在运行的 Agent 任务" else "Active agent task" } else {
} else if (data.runs.isNotEmpty()) { if (zh) "最近 Agent 运行" else "Latest agent run"
if (zh) "最近 Agent 运行" else "Latest agent run" },
} else { subtitle = if (task != null) {
if (zh) "暂无进展更新" else "No progress updates yet" listOf(task.status, clean(task.triggerSummary).ifBlank { Models.shortId(task.id) })
},
subtitle = if (data.activeTasks.isNotEmpty()) {
if (zh) "${data.activeTasks.size} 个任务正在运行" else "${data.activeTasks.size} active task(s)"
} else if (data.runs.isNotEmpty()) {
if (zh) "${data.runs.size} 条 Agent 运行记录" else "${data.runs.size} agent run(s)"
} else {
if (zh) "Agent live card 对齐 Web activity header" else "Agent live card aligned with the Web activity header"
},
icon = Icons.Outlined.Bolt,
tone = if (data.activeTasks.isNotEmpty()) MulticaButtonTone.Primary else MulticaButtonTone.Ghost,
contentDescription = "Issue Agent Live Web Summary $WEB_SSOT_EXECUTION_LOG_SECTION",
)
}
item {
when {
data.activeTasks.isNotEmpty() -> {
val task = data.activeTasks.first()
IssueDetailProgressCard(
title = if (zh) "活跃 Agent 任务" else "Active agent task",
subtitle = listOf(task.status, clean(task.triggerSummary).ifBlank { Models.shortId(task.id) })
.filter { it.isNotBlank() } .filter { it.isNotBlank() }
.joinToString(" · "), .joinToString(" · ")
icon = Icons.Outlined.Bolt, } else {
tone = MulticaButtonTone.Primary, listOf(run?.status.orEmpty(), run?.startedAt?.let { shortDate(it) }.orEmpty(), clean(run?.error))
)
}
data.runs.isNotEmpty() -> {
val run = data.runs.first()
IssueDetailProgressCard(
title = if (zh) "最近 Agent 运行" else "Latest agent run",
subtitle = listOf(run.status, shortDate(run.startedAt), clean(run.error))
.filter { it.isNotBlank() } .filter { it.isNotBlank() }
.joinToString(" · "), .joinToString(" · ")
icon = Icons.Outlined.Bolt, },
tone = if (run.status == "failed") MulticaButtonTone.Destructive else MulticaButtonTone.Primary, icon = Icons.Outlined.Bolt,
onClick = { selectedRun = run }, tone = if (task != null || run?.status != "failed") MulticaButtonTone.Primary else MulticaButtonTone.Destructive,
) contentDescription = "Issue Agent Progress Compact Row $WEB_SSOT_EXECUTION_LOG_SECTION",
} onClick = if (run != null) ({ selectedRun = run }) else null,
!data.timeline?.entries.isNullOrEmpty() -> { )
val entry = data.timeline!!.entries.first()
IssueDetailProgressCard(
title = clean(entry.action).ifBlank { if (zh) "最近活动" else "Latest activity" },
subtitle = clean(entry.content).ifBlank { shortDate(entry.createdAt) },
icon = Icons.Outlined.CalendarMonth,
tone = MulticaButtonTone.Ghost,
)
}
else -> {
IssueDetailProgressCard(
title = if (zh) "暂无进展更新" else "No progress updates yet",
subtitle = if (zh) "Agent 运行、评论和活动会优先显示在这里" else "Agent runs, comments, and activity will surface here first",
icon = Icons.Outlined.RadioButtonUnchecked,
tone = MulticaButtonTone.Ghost,
)
}
} }
} }
item { item {
@@ -10795,12 +10800,6 @@ private fun IssueCommentInputBar(
.padding(horizontal = 18.dp, vertical = 10.dp), .padding(horizontal = 18.dp, vertical = 10.dp),
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
) { ) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(1.dp)
.background(MulticaColors.Border)
)
if (pendingAttachments.isNotEmpty()) { if (pendingAttachments.isNotEmpty()) {
Text( Text(
text = if (zh) { text = if (zh) {
@@ -10830,17 +10829,21 @@ private fun IssueCommentInputBar(
horizontalArrangement = Arrangement.spacedBy(7.dp), horizontalArrangement = Arrangement.spacedBy(7.dp),
verticalAlignment = Alignment.Bottom, verticalAlignment = Alignment.Bottom,
) { ) {
MulticaPillButton( MulticaIconPillButton(
text = if (uploadingAttachment) "..." else if (zh) "" else "Img", icon = Icons.Outlined.Image,
contentDescription = "Issue Comment Attach Image",
onClick = onAttachImage, onClick = onAttachImage,
tone = MulticaButtonTone.Secondary, tone = MulticaButtonTone.Secondary,
contentDescription = "Issue Comment Attach Image", enabled = !uploadingAttachment,
modifier = Modifier.size(38.dp),
) )
MulticaPillButton( MulticaIconPillButton(
text = "+", icon = Icons.Outlined.Add,
contentDescription = "Issue Comment Attach",
onClick = onAttach, onClick = onAttach,
tone = MulticaButtonTone.Ghost, tone = MulticaButtonTone.Ghost,
contentDescription = "Issue Comment Attach", enabled = !uploadingAttachment,
modifier = Modifier.size(38.dp),
) )
MulticaPillButton( MulticaPillButton(
text = "@", text = "@",
@@ -10852,7 +10855,9 @@ private fun IssueCommentInputBar(
value = draft, value = draft,
onValueChange = onDraftChange, onValueChange = onDraftChange,
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
label = if (zh) "评论" else "Comment", label = "",
placeholder = if (zh) "评论" else "Comment",
showLabel = false,
contentDescription = "Issue Comment Input", contentDescription = "Issue Comment Input",
minLines = 1, minLines = 1,
maxLines = 4, maxLines = 4,
@@ -1596,6 +1596,8 @@ fun MulticaCupertinoTextField(
label: String, label: String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
contentDescription: String? = null, contentDescription: String? = null,
placeholder: String = label,
showLabel: Boolean = true,
enabled: Boolean = true, enabled: Boolean = true,
singleLine: Boolean = false, singleLine: Boolean = false,
minLines: Int = 1, minLines: Int = 1,
@@ -1609,13 +1611,15 @@ fun MulticaCupertinoTextField(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(5.dp), verticalArrangement = Arrangement.spacedBy(5.dp),
) { ) {
Text( if (showLabel && label.isNotBlank()) {
text = label, Text(
style = MaterialTheme.typography.labelMedium.copy(fontSize = 12.sp, lineHeight = 15.sp, fontWeight = FontWeight.SemiBold), text = label,
color = MulticaColors.TextTertiary, style = MaterialTheme.typography.labelMedium.copy(fontSize = 12.sp, lineHeight = 15.sp, fontWeight = FontWeight.SemiBold),
maxLines = 1, color = MulticaColors.TextTertiary,
overflow = TextOverflow.Ellipsis, maxLines = 1,
) overflow = TextOverflow.Ellipsis,
)
}
CupertinoBorderedTextField( CupertinoBorderedTextField(
value = value, value = value,
onValueChange = onValueChange, onValueChange = onValueChange,
@@ -1624,7 +1628,11 @@ fun MulticaCupertinoTextField(
singleLine = singleLine, singleLine = singleLine,
minLines = minLines, minLines = minLines,
maxLines = maxLines, maxLines = maxLines,
placeholder = { Text(label, color = MulticaColors.TextTertiary) }, placeholder = {
if (placeholder.isNotBlank()) {
Text(placeholder, color = MulticaColors.TextTertiary)
}
},
textStyle = MaterialTheme.typography.bodyLarge.copy(fontSize = 16.sp, lineHeight = 21.sp, color = MulticaColors.TextPrimary), textStyle = MaterialTheme.typography.bodyLarge.copy(fontSize = 16.sp, lineHeight = 21.sp, color = MulticaColors.TextPrimary),
shape = RoundedCornerShape(10.dp), shape = RoundedCornerShape(10.dp),
) )
@@ -1639,6 +1647,8 @@ fun MulticaTextField(
label: String, label: String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
contentDescription: String? = null, contentDescription: String? = null,
placeholder: String = label,
showLabel: Boolean = true,
enabled: Boolean = true, enabled: Boolean = true,
singleLine: Boolean = false, singleLine: Boolean = false,
minLines: Int = 1, minLines: Int = 1,
@@ -1650,6 +1660,8 @@ fun MulticaTextField(
label = label, label = label,
modifier = modifier, modifier = modifier,
contentDescription = contentDescription, contentDescription = contentDescription,
placeholder = placeholder,
showLabel = showLabel,
enabled = enabled, enabled = enabled,
singleLine = singleLine, singleLine = singleLine,
minLines = minLines, minLines = minLines,