diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/components/ChatLifecycleEffects.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/components/ChatLifecycleEffects.kt
index e8cfd3bc..b6161f6d 100644
--- a/app/src/main/java/com/m57/hermescontrol/ui/chat/components/ChatLifecycleEffects.kt
+++ b/app/src/main/java/com/m57/hermescontrol/ui/chat/components/ChatLifecycleEffects.kt
@@ -1,17 +1,23 @@
package com.m57.hermescontrol.ui.chat.components
import android.Manifest
+import android.content.ClipData
import android.content.pm.PackageManager
import android.os.Build
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.SnackbarResult
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.platform.ClipEntry
+import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle
@@ -19,6 +25,7 @@ import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.repeatOnLifecycle
import com.m57.hermescontrol.NavigationController
+import com.m57.hermescontrol.R
import com.m57.hermescontrol.data.ws.ConnectionStatus
import com.m57.hermescontrol.notification.ReplyNotificationTracker
import com.m57.hermescontrol.notification.correlationScopeId
@@ -28,6 +35,7 @@ import com.m57.hermescontrol.ui.chat.ClarifyUi
import com.m57.hermescontrol.ui.chat.SecretPromptUi
import com.m57.hermescontrol.ui.chat.SudoPromptUi
import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.launch
@Composable
fun ChatLifecycleEffects(
@@ -137,10 +145,24 @@ fun ChatLifecycleEffects(
}
// Show error as snackbar
+ val clipboard = LocalClipboard.current
+ val copyLabel = context.getString(R.string.action_copy_error)
+ // Own scope + immediate clear: a later `errorMessage = null` elsewhere must not cancel a visible popup.
+ val snackbarScope = rememberCoroutineScope()
LaunchedEffect(errorMessage) {
errorMessage?.let { error ->
- snackbarHostState.showSnackbar(error)
viewModel.clearError()
+ snackbarScope.launch {
+ val result =
+ snackbarHostState.showSnackbar(
+ error,
+ actionLabel = copyLabel,
+ duration = SnackbarDuration.Long,
+ )
+ if (result == SnackbarResult.ActionPerformed) {
+ clipboard.setClipEntry(ClipEntry(ClipData.newPlainText(null, error)))
+ }
+ }
}
}
diff --git a/app/src/main/java/com/m57/hermescontrol/ui/common/StateViews.kt b/app/src/main/java/com/m57/hermescontrol/ui/common/StateViews.kt
index 4f9bd413..027bce0d 100644
--- a/app/src/main/java/com/m57/hermescontrol/ui/common/StateViews.kt
+++ b/app/src/main/java/com/m57/hermescontrol/ui/common/StateViews.kt
@@ -1,5 +1,6 @@
package com.m57.hermescontrol.ui.common
+import android.content.ClipData
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
@@ -22,6 +23,7 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.material.icons.filled.Inbox
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.Button
@@ -30,8 +32,10 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -39,6 +43,8 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.platform.ClipEntry
+import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
@@ -48,6 +54,7 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.m57.hermescontrol.R
import com.m57.hermescontrol.theme.LocalSpacing
+import kotlinx.coroutines.launch
// Unified loading / error / empty / skeleton placeholders so every screen
// shares the same visual language.
@@ -96,6 +103,8 @@ fun ErrorState(
modifier: Modifier = Modifier,
) {
val spacing = LocalSpacing.current
+ val clipboard = LocalClipboard.current
+ val scope = rememberCoroutineScope()
Column(
modifier =
modifier
@@ -117,6 +126,20 @@ fun ErrorState(
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyMedium,
)
+ TextButton(
+ onClick = {
+ scope.launch { clipboard.setClipEntry(ClipEntry(ClipData.newPlainText(null, message))) }
+ },
+ modifier = Modifier.testTag("error_copy_button"),
+ ) {
+ Icon(
+ imageVector = Icons.Filled.ContentCopy,
+ contentDescription = null,
+ modifier = Modifier.size(16.dp),
+ )
+ Spacer(modifier = Modifier.size(spacing.xs))
+ Text(stringResource(R.string.action_copy_error))
+ }
if (onRetry != null) {
Spacer(modifier = Modifier.height(spacing.md))
Button(
diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
index 6a328570..8e923ad0 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -19,6 +19,7 @@
النتيجة التالية
إغلاق البحث
إعادة المحاولة
+نسخ الخطأ
إلغاء
تأكيد
مسح
diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml
index fabaaf82..ee02ab7a 100644
--- a/app/src/main/res/values-ko/strings.xml
+++ b/app/src/main/res/values-ko/strings.xml
@@ -22,6 +22,7 @@
재시도
+ 오류 복사
취소
지우기
확인
diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml
index 67868a7b..6944c427 100644
--- a/app/src/main/res/values-zh/strings.xml
+++ b/app/src/main/res/values-zh/strings.xml
@@ -19,6 +19,7 @@
下一个匹配
关闭搜索
重试
+ 复制错误
取消
确认
清除
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 2bca4211..eb91aa7a 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -29,6 +29,7 @@
Retry
+ Copy error
Cancel
Confirm
Clear