diff --git a/app/src/main/java/com/aicode/feature/agent/presentation/component/AIChatPanel.kt b/app/src/main/java/com/aicode/feature/agent/presentation/component/AIChatPanel.kt index 603e939c..f4811943 100644 --- a/app/src/main/java/com/aicode/feature/agent/presentation/component/AIChatPanel.kt +++ b/app/src/main/java/com/aicode/feature/agent/presentation/component/AIChatPanel.kt @@ -91,6 +91,8 @@ import java.io.File import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock /** @@ -491,6 +493,8 @@ fun AIChatPanel( } var pendingAttachments by remember { mutableStateOf>(emptyList()) } var uploadingCount by remember { mutableStateOf(0) } + // 串行化附件上传:两次上传并发时 pendingAttachments 的读-改-写会互相覆盖,导致先选的附件预览丢失。 + val attachmentUploadMutex = remember { Mutex() } var messageForMenu by remember { mutableStateOf(null) } var editingMessage by remember { mutableStateOf(null) } val listState = rememberLazyListState() @@ -709,20 +713,23 @@ fun AIChatPanel( scope.launch { var successCount = 0 val failures = mutableListOf() - uploadingCount = selected.size - try { - selected.forEach { uri -> - runCatching { - copyUriToWorkspace(context, uri, viewModel.fileAccess, includeImageData = images) - }.onSuccess { uploaded -> - pendingAttachments = pendingAttachments + uploaded.toPendingAttachment() - successCount += 1 - }.onFailure { error -> - failures += (error.message ?: uploadFallbackError(context)) + // 串行化:并发上传会让 pendingAttachments 的读-改-写互相覆盖,先选的附件预览丢失。 + attachmentUploadMutex.withLock { + uploadingCount = selected.size + try { + selected.forEach { uri -> + runCatching { + copyUriToWorkspace(context, uri, viewModel.fileAccess, includeImageData = images) + }.onSuccess { uploaded -> + pendingAttachments = pendingAttachments + uploaded.toPendingAttachment() + successCount += 1 + }.onFailure { error -> + failures += (error.message ?: uploadFallbackError(context)) + } } + } finally { + uploadingCount = 0 } - } finally { - uploadingCount = 0 } // 结果提示:全失败展示首个错误;有文件被上限截断或上传失败时用 partial 文案;全成功用 success 文案。 val skipped = uris.size - selected.size diff --git a/app/src/main/java/com/aicode/feature/agent/presentation/component/ChatInputAttachments.kt b/app/src/main/java/com/aicode/feature/agent/presentation/component/ChatInputAttachments.kt index dd857db2..7a65e385 100644 --- a/app/src/main/java/com/aicode/feature/agent/presentation/component/ChatInputAttachments.kt +++ b/app/src/main/java/com/aicode/feature/agent/presentation/component/ChatInputAttachments.kt @@ -32,6 +32,7 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.produceState import androidx.compose.runtime.remember @@ -148,10 +149,15 @@ internal fun PendingAttachmentPreviewList( ) { if (attachments.isEmpty()) return + val scrollState = rememberScrollState() + // 新附件加入时自动滚到最右,保证刚上传的附件可见(横向滑动,不改竖向)。 + LaunchedEffect(attachments.size) { + scrollState.animateScrollTo(scrollState.maxValue) + } Row( modifier = Modifier .fillMaxWidth() - .horizontalScroll(rememberScrollState()) + .horizontalScroll(scrollState) .padding(horizontal = Spacing.xs, vertical = Spacing.xs), horizontalArrangement = Arrangement.spacedBy(Spacing.sm) ) {