@@ -12,6 +12,8 @@ data class ServerStoreState(
|
|||||||
val port: Int = 9119,
|
val port: Int = 9119,
|
||||||
val baseUrl: String? = null,
|
val baseUrl: String? = null,
|
||||||
val autoReconnect: Boolean = true,
|
val autoReconnect: Boolean = true,
|
||||||
|
val restoreLastSession: Boolean = false,
|
||||||
|
val lastOpenedSessionIds: Map<String, String> = emptyMap(),
|
||||||
val themePreference: ThemePreference = ThemePreference.SYSTEM,
|
val themePreference: ThemePreference = ThemePreference.SYSTEM,
|
||||||
val useDynamicColors: Boolean = false,
|
val useDynamicColors: Boolean = false,
|
||||||
val themePreset: ThemePreset = ThemePreset.DEFAULT,
|
val themePreset: ThemePreset = ThemePreset.DEFAULT,
|
||||||
|
|||||||
@@ -616,6 +616,47 @@ object AuthManager {
|
|||||||
serverStore.update { it.copy(autoReconnect = enabled) }
|
serverStore.update { it.copy(autoReconnect = enabled) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Restore last session on startup (issue #1102) ─────────────────────
|
||||||
|
|
||||||
|
private fun currentSessionScopeKey(): String {
|
||||||
|
val connId = getSelectedProfileId()?.takeIf { it.isNotBlank() } ?: DEFAULT_PROFILE_ID
|
||||||
|
val serverProfile = activeProfileId.value?.takeIf { it.isNotBlank() } ?: DEFAULT_PROFILE_ID
|
||||||
|
return "$connId:$serverProfile"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isRestoreLastSession(): Boolean = serverStore.getLatestState().restoreLastSession
|
||||||
|
|
||||||
|
fun setRestoreLastSession(enabled: Boolean) {
|
||||||
|
serverStore.update { it.copy(restoreLastSession = enabled) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getLastOpenedSessionId(): String? = serverStore.getLatestState().lastOpenedSessionIds[currentSessionScopeKey()]
|
||||||
|
|
||||||
|
fun setLastOpenedSessionId(sessionId: String?) {
|
||||||
|
val key = currentSessionScopeKey()
|
||||||
|
serverStore.update { state ->
|
||||||
|
val updated =
|
||||||
|
if (sessionId != null) {
|
||||||
|
state.lastOpenedSessionIds + (key to sessionId)
|
||||||
|
} else {
|
||||||
|
state.lastOpenedSessionIds - key
|
||||||
|
}
|
||||||
|
state.copy(lastOpenedSessionIds = updated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearLastOpenedSessionId() {
|
||||||
|
setLastOpenedSessionId(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearLastOpenedSessionIdsForConnection(connectionProfileId: String) {
|
||||||
|
serverStore.update { state ->
|
||||||
|
val prefix = "$connectionProfileId:"
|
||||||
|
val updated = state.lastOpenedSessionIds.filterKeys { !it.startsWith(prefix) }
|
||||||
|
state.copy(lastOpenedSessionIds = updated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Theme preference ──────────────────────────────────────────────────
|
// ── Theme preference ──────────────────────────────────────────────────
|
||||||
|
|
||||||
fun getThemePreference(): ThemePreference = serverStore.getLatestState().themePreference
|
fun getThemePreference(): ThemePreference = serverStore.getLatestState().themePreference
|
||||||
|
|||||||
@@ -677,6 +677,13 @@ class ChatViewModel(
|
|||||||
if (!initial.isNullOrBlank()) {
|
if (!initial.isNullOrBlank()) {
|
||||||
initialSessionId = null
|
initialSessionId = null
|
||||||
switchSession(initial)
|
switchSession(initial)
|
||||||
|
} else if (AuthManager.isRestoreLastSession()) {
|
||||||
|
val restoredId = AuthManager.getLastOpenedSessionId()
|
||||||
|
if (!restoredId.isNullOrBlank()) {
|
||||||
|
switchSession(restoredId)
|
||||||
|
} else {
|
||||||
|
createNewSession(setLoading = false)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
createNewSession(setLoading = false)
|
createNewSession(setLoading = false)
|
||||||
}
|
}
|
||||||
@@ -1406,6 +1413,7 @@ class ChatViewModel(
|
|||||||
userMessage: ChatMessage? = null,
|
userMessage: ChatMessage? = null,
|
||||||
) {
|
) {
|
||||||
val dispatchGeneration = sessionGeneration
|
val dispatchGeneration = sessionGeneration
|
||||||
|
AuthManager.setLastOpenedSessionId(storageSessionId)
|
||||||
val msgToPersist =
|
val msgToPersist =
|
||||||
userMessage ?: ChatMessage(
|
userMessage ?: ChatMessage(
|
||||||
role = MessageRole.USER,
|
role = MessageRole.USER,
|
||||||
@@ -2356,6 +2364,7 @@ class ChatViewModel(
|
|||||||
.find { it.id == sessionId }
|
.find { it.id == sessionId }
|
||||||
?.title ?: "Hermes"
|
?.title ?: "Hermes"
|
||||||
val generation = resetSessionState(sessionId, title, isLoading = true)
|
val generation = resetSessionState(sessionId, title, isLoading = true)
|
||||||
|
AuthManager.setLastOpenedSessionId(sessionId)
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
// Warm-cache fast-path (desktop parity): paint the cached Room
|
// Warm-cache fast-path (desktop parity): paint the cached Room
|
||||||
// transcript immediately so the screen never sits blank, then load
|
// transcript immediately so the screen never sits blank, then load
|
||||||
@@ -2708,6 +2717,9 @@ class ChatViewModel(
|
|||||||
}
|
}
|
||||||
sessionGoneRecoveryInFlight = true
|
sessionGoneRecoveryInFlight = true
|
||||||
cancelResumeRetry()
|
cancelResumeRetry()
|
||||||
|
if (AuthManager.getLastOpenedSessionId() == sessionId) {
|
||||||
|
AuthManager.clearLastOpenedSessionId()
|
||||||
|
}
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.m57.hermescontrol.ui.sessions
|
|||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.m57.hermescontrol.data.local.AuthManager
|
||||||
import com.m57.hermescontrol.data.model.BulkDeleteRequest
|
import com.m57.hermescontrol.data.model.BulkDeleteRequest
|
||||||
import com.m57.hermescontrol.data.model.PruneRequest
|
import com.m57.hermescontrol.data.model.PruneRequest
|
||||||
import com.m57.hermescontrol.data.model.SessionInfo
|
import com.m57.hermescontrol.data.model.SessionInfo
|
||||||
@@ -689,6 +690,9 @@ class SessionsViewModel(
|
|||||||
}
|
}
|
||||||
when (result) {
|
when (result) {
|
||||||
is NetworkResult.Success -> {
|
is NetworkResult.Success -> {
|
||||||
|
if (AuthManager.getLastOpenedSessionId() == sessionId) {
|
||||||
|
AuthManager.clearLastOpenedSessionId()
|
||||||
|
}
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
val updatedSessions = it.sessions.filter { s -> s.id != sessionId }
|
val updatedSessions = it.sessions.filter { s -> s.id != sessionId }
|
||||||
val newTotal = (it.total - 1).coerceAtLeast(0)
|
val newTotal = (it.total - 1).coerceAtLeast(0)
|
||||||
@@ -739,6 +743,10 @@ class SessionsViewModel(
|
|||||||
}
|
}
|
||||||
when (result) {
|
when (result) {
|
||||||
is NetworkResult.Success -> {
|
is NetworkResult.Success -> {
|
||||||
|
val lastOpened = AuthManager.getLastOpenedSessionId()
|
||||||
|
if (lastOpened != null && ids.contains(lastOpened)) {
|
||||||
|
AuthManager.clearLastOpenedSessionId()
|
||||||
|
}
|
||||||
val deletedCount = result.data.deleted
|
val deletedCount = result.data.deleted
|
||||||
val toastMsg =
|
val toastMsg =
|
||||||
if (deletedCount > 0) {
|
if (deletedCount > 0) {
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ fun SettingsScreen(
|
|||||||
icon = Icons.Filled.Tune,
|
icon = Icons.Filled.Tune,
|
||||||
label = stringResource(R.string.settings_sec_behavior),
|
label = stringResource(R.string.settings_sec_behavior),
|
||||||
summary =
|
summary =
|
||||||
if (state.autoReconnect) {
|
if (state.autoReconnect || state.restoreLastSession) {
|
||||||
stringResource(R.string.settings_summary_on)
|
stringResource(R.string.settings_summary_on)
|
||||||
} else {
|
} else {
|
||||||
stringResource(R.string.settings_summary_off)
|
stringResource(R.string.settings_summary_off)
|
||||||
|
|||||||
@@ -227,6 +227,8 @@ internal fun SettingsBehaviorPage(
|
|||||||
BehaviorSection(
|
BehaviorSection(
|
||||||
autoReconnect = state.autoReconnect,
|
autoReconnect = state.autoReconnect,
|
||||||
onAutoReconnectChange = viewModel::onAutoReconnectChange,
|
onAutoReconnectChange = viewModel::onAutoReconnectChange,
|
||||||
|
restoreLastSession = state.restoreLastSession,
|
||||||
|
onRestoreLastSessionChange = viewModel::onRestoreLastSessionChange,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ data class SettingsUiState(
|
|||||||
val transportWarning: String? = null,
|
val transportWarning: String? = null,
|
||||||
val token: String = "",
|
val token: String = "",
|
||||||
val autoReconnect: Boolean = true,
|
val autoReconnect: Boolean = true,
|
||||||
|
val restoreLastSession: Boolean = false,
|
||||||
val themePreference: ThemePreference = ThemePreference.SYSTEM,
|
val themePreference: ThemePreference = ThemePreference.SYSTEM,
|
||||||
val useDynamicColors: Boolean = false,
|
val useDynamicColors: Boolean = false,
|
||||||
val themePreset: ThemePreset = ThemePreset.DEFAULT,
|
val themePreset: ThemePreset = ThemePreset.DEFAULT,
|
||||||
@@ -72,6 +73,7 @@ class SettingsViewModel(
|
|||||||
val baseUrl = AuthManager.getBaseUrl()
|
val baseUrl = AuthManager.getBaseUrl()
|
||||||
val token = AuthManager.getToken() ?: ""
|
val token = AuthManager.getToken() ?: ""
|
||||||
val autoReconnect = AuthManager.isAutoReconnect()
|
val autoReconnect = AuthManager.isAutoReconnect()
|
||||||
|
val restoreLastSession = AuthManager.isRestoreLastSession()
|
||||||
val themePreference = AuthManager.getThemePreference()
|
val themePreference = AuthManager.getThemePreference()
|
||||||
val useDynamicColors = AuthManager.isUseDynamicColors()
|
val useDynamicColors = AuthManager.isUseDynamicColors()
|
||||||
val themePreset = AuthManager.getThemePreset()
|
val themePreset = AuthManager.getThemePreset()
|
||||||
@@ -92,6 +94,7 @@ class SettingsViewModel(
|
|||||||
transportWarning = transportWarning,
|
transportWarning = transportWarning,
|
||||||
token = token,
|
token = token,
|
||||||
autoReconnect = autoReconnect,
|
autoReconnect = autoReconnect,
|
||||||
|
restoreLastSession = restoreLastSession,
|
||||||
themePreference = themePreference,
|
themePreference = themePreference,
|
||||||
useDynamicColors = useDynamicColors,
|
useDynamicColors = useDynamicColors,
|
||||||
themePreset = themePreset,
|
themePreset = themePreset,
|
||||||
@@ -120,6 +123,7 @@ class SettingsViewModel(
|
|||||||
val updatedProfiles = AuthManager.getConnectionProfiles().filter { it.id != profileId }
|
val updatedProfiles = AuthManager.getConnectionProfiles().filter { it.id != profileId }
|
||||||
AuthManager.saveConnectionProfiles(updatedProfiles)
|
AuthManager.saveConnectionProfiles(updatedProfiles)
|
||||||
AuthManager.setProfileToken(profileId, null)
|
AuthManager.setProfileToken(profileId, null)
|
||||||
|
AuthManager.clearLastOpenedSessionIdsForConnection(profileId)
|
||||||
// Never leave selection null (issue #478): if the deleted profile was selected,
|
// Never leave selection null (issue #478): if the deleted profile was selected,
|
||||||
// fall back to the default profile instead of clearing selection.
|
// fall back to the default profile instead of clearing selection.
|
||||||
val reselectDefault =
|
val reselectDefault =
|
||||||
@@ -283,6 +287,11 @@ class SettingsViewModel(
|
|||||||
AuthManager.setAutoReconnect(enabled)
|
AuthManager.setAutoReconnect(enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onRestoreLastSessionChange(enabled: Boolean) {
|
||||||
|
_uiState.update { it.copy(restoreLastSession = enabled, isSaved = false) }
|
||||||
|
AuthManager.setRestoreLastSession(enabled)
|
||||||
|
}
|
||||||
|
|
||||||
fun onThemeChange(theme: ThemePreference) {
|
fun onThemeChange(theme: ThemePreference) {
|
||||||
_uiState.update { it.copy(themePreference = theme, isSaved = false) }
|
_uiState.update { it.copy(themePreference = theme, isSaved = false) }
|
||||||
AuthManager.setThemePreference(theme)
|
AuthManager.setThemePreference(theme)
|
||||||
@@ -323,6 +332,7 @@ class SettingsViewModel(
|
|||||||
AuthManager.setToken(null)
|
AuthManager.setToken(null)
|
||||||
AuthManager.setSessionCookie(null)
|
AuthManager.setSessionCookie(null)
|
||||||
AuthManager.setWsAuthParam("token")
|
AuthManager.setWsAuthParam("token")
|
||||||
|
AuthManager.clearLastOpenedSessionId()
|
||||||
HermesWsClient.disconnect(clearPendingMessages = true)
|
HermesWsClient.disconnect(clearPendingMessages = true)
|
||||||
// Don't rebuild ApiClient here — let the navigation complete first
|
// Don't rebuild ApiClient here — let the navigation complete first
|
||||||
}
|
}
|
||||||
@@ -337,6 +347,7 @@ class SettingsViewModel(
|
|||||||
AuthManager.setBaseUrl(normalized)
|
AuthManager.setBaseUrl(normalized)
|
||||||
AuthManager.setToken(state.token)
|
AuthManager.setToken(state.token)
|
||||||
AuthManager.setAutoReconnect(state.autoReconnect)
|
AuthManager.setAutoReconnect(state.autoReconnect)
|
||||||
|
AuthManager.setRestoreLastSession(state.restoreLastSession)
|
||||||
// B6 (Jun 18 2026, kanban t_86e9be9b): persist theme choice so it
|
// B6 (Jun 18 2026, kanban t_86e9be9b): persist theme choice so it
|
||||||
// survives a cold start (was previously dropped on save()).
|
// survives a cold start (was previously dropped on save()).
|
||||||
AuthManager.setThemePreference(state.themePreference)
|
AuthManager.setThemePreference(state.themePreference)
|
||||||
|
|||||||
@@ -3,17 +3,19 @@ package com.m57.hermescontrol.ui.settings.components
|
|||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.SwitchDefaults
|
import androidx.compose.material3.SwitchDefaults
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import com.m57.hermescontrol.R
|
import com.m57.hermescontrol.R
|
||||||
import com.m57.hermescontrol.ui.settings.SectionCard
|
import com.m57.hermescontrol.ui.settings.SectionCard
|
||||||
|
|
||||||
@@ -21,6 +23,8 @@ import com.m57.hermescontrol.ui.settings.SectionCard
|
|||||||
internal fun BehaviorSection(
|
internal fun BehaviorSection(
|
||||||
autoReconnect: Boolean,
|
autoReconnect: Boolean,
|
||||||
onAutoReconnectChange: (Boolean) -> Unit,
|
onAutoReconnectChange: (Boolean) -> Unit,
|
||||||
|
restoreLastSession: Boolean,
|
||||||
|
onRestoreLastSessionChange: (Boolean) -> Unit,
|
||||||
) {
|
) {
|
||||||
SectionCard {
|
SectionCard {
|
||||||
Row(
|
Row(
|
||||||
@@ -28,7 +32,7 @@ internal fun BehaviorSection(
|
|||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Column {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.settings_item_auto_reconnect),
|
text = stringResource(R.string.settings_item_auto_reconnect),
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
@@ -51,5 +55,38 @@ internal fun BehaviorSection(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.settings_item_restore_session),
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.settings_desc_restore_session),
|
||||||
|
style =
|
||||||
|
MaterialTheme.typography.bodySmall.copy(
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Switch(
|
||||||
|
checked = restoreLastSession,
|
||||||
|
onCheckedChange = onRestoreLastSessionChange,
|
||||||
|
colors =
|
||||||
|
SwitchDefaults.colors(
|
||||||
|
checkedThumbColor = MaterialTheme.colorScheme.primary,
|
||||||
|
checkedTrackColor = MaterialTheme.colorScheme.primaryContainer,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -177,6 +177,8 @@
|
|||||||
<string name="settings_sec_behavior">السلوك</string>
|
<string name="settings_sec_behavior">السلوك</string>
|
||||||
<string name="settings_item_auto_reconnect">إعادة الاتصال التلقائي</string>
|
<string name="settings_item_auto_reconnect">إعادة الاتصال التلقائي</string>
|
||||||
<string name="settings_desc_auto_reconnect">إعادة الاتصال تلقائيًا عند انقطاعه</string>
|
<string name="settings_desc_auto_reconnect">إعادة الاتصال تلقائيًا عند انقطاعه</string>
|
||||||
|
<string name="settings_item_restore_session">استعادة الجلسة الأخيرة</string>
|
||||||
|
<string name="settings_desc_restore_session">إعادة فتح أحدث جلسة محادثة عند بدء التشغيل</string>
|
||||||
<string name="settings_sec_appearance">المظهر</string>
|
<string name="settings_sec_appearance">المظهر</string>
|
||||||
<string name="settings_item_theme">السمة</string>
|
<string name="settings_item_theme">السمة</string>
|
||||||
<string name="settings_sec_chat">المحادثة</string>
|
<string name="settings_sec_chat">المحادثة</string>
|
||||||
|
|||||||
@@ -180,6 +180,8 @@
|
|||||||
<string name="settings_sec_behavior">동작</string>
|
<string name="settings_sec_behavior">동작</string>
|
||||||
<string name="settings_item_auto_reconnect">자동 재연결</string>
|
<string name="settings_item_auto_reconnect">자동 재연결</string>
|
||||||
<string name="settings_desc_auto_reconnect">연결 끊김 시 자동 재연결</string>
|
<string name="settings_desc_auto_reconnect">연결 끊김 시 자동 재연결</string>
|
||||||
|
<string name="settings_item_restore_session">마지막 세션 복원</string>
|
||||||
|
<string name="settings_desc_restore_session">시작 시 가장 최근 채팅 세션을 다시 엽니다</string>
|
||||||
<string name="settings_sec_appearance">테마</string>
|
<string name="settings_sec_appearance">테마</string>
|
||||||
<string name="settings_item_theme">테마</string>
|
<string name="settings_item_theme">테마</string>
|
||||||
<string name="settings_sec_chat">채팅</string>
|
<string name="settings_sec_chat">채팅</string>
|
||||||
|
|||||||
@@ -177,6 +177,8 @@
|
|||||||
<string name="settings_sec_behavior">行为</string>
|
<string name="settings_sec_behavior">行为</string>
|
||||||
<string name="settings_item_auto_reconnect">自动重连</string>
|
<string name="settings_item_auto_reconnect">自动重连</string>
|
||||||
<string name="settings_desc_auto_reconnect">断开后自动重新连接</string>
|
<string name="settings_desc_auto_reconnect">断开后自动重新连接</string>
|
||||||
|
<string name="settings_item_restore_session">恢复上次会话</string>
|
||||||
|
<string name="settings_desc_restore_session">启动时重新打开最近的聊天会话</string>
|
||||||
<string name="settings_sec_appearance">外观</string>
|
<string name="settings_sec_appearance">外观</string>
|
||||||
<string name="settings_item_theme">主题</string>
|
<string name="settings_item_theme">主题</string>
|
||||||
<string name="settings_sec_chat">对话</string>
|
<string name="settings_sec_chat">对话</string>
|
||||||
|
|||||||
@@ -201,6 +201,8 @@
|
|||||||
<string name="settings_sec_behavior">Behavior</string>
|
<string name="settings_sec_behavior">Behavior</string>
|
||||||
<string name="settings_item_auto_reconnect">Auto-Reconnect</string>
|
<string name="settings_item_auto_reconnect">Auto-Reconnect</string>
|
||||||
<string name="settings_desc_auto_reconnect">Automatically reconnect on disconnect</string>
|
<string name="settings_desc_auto_reconnect">Automatically reconnect on disconnect</string>
|
||||||
|
<string name="settings_item_restore_session">Restore Last Session</string>
|
||||||
|
<string name="settings_desc_restore_session">Reopen the most recent chat session on startup</string>
|
||||||
<string name="settings_sec_appearance">Appearance</string>
|
<string name="settings_sec_appearance">Appearance</string>
|
||||||
<string name="settings_item_theme">Theme</string>
|
<string name="settings_item_theme">Theme</string>
|
||||||
<string name="settings_sec_chat">Chat</string>
|
<string name="settings_sec_chat">Chat</string>
|
||||||
|
|||||||
@@ -90,6 +90,10 @@ class ChatUpdateCommandTest {
|
|||||||
every { AuthManager.isTypingEffectEnabled() } returns true
|
every { AuthManager.isTypingEffectEnabled() } returns true
|
||||||
every { AuthManager.getTypingEffectDelayMs() } returns 30
|
every { AuthManager.getTypingEffectDelayMs() } returns 30
|
||||||
every { AuthManager.isAutoReconnect() } returns false
|
every { AuthManager.isAutoReconnect() } returns false
|
||||||
|
every { AuthManager.isRestoreLastSession() } returns false
|
||||||
|
every { AuthManager.getLastOpenedSessionId() } returns null
|
||||||
|
every { AuthManager.setLastOpenedSessionId(any()) } returns Unit
|
||||||
|
every { AuthManager.clearLastOpenedSessionId() } returns Unit
|
||||||
every { HermesWsClient.events } returns mockEventsFlow
|
every { HermesWsClient.events } returns mockEventsFlow
|
||||||
every { HermesWsClient.connectionStatus } returns mockConnectionStatus
|
every { HermesWsClient.connectionStatus } returns mockConnectionStatus
|
||||||
every { HermesWsClient.connect() } answers {
|
every { HermesWsClient.connect() } answers {
|
||||||
|
|||||||
@@ -164,6 +164,10 @@ class ChatViewModelTest {
|
|||||||
every { AuthManager.isTypingEffectEnabled() } returns true
|
every { AuthManager.isTypingEffectEnabled() } returns true
|
||||||
every { AuthManager.getTypingEffectDelayMs() } returns 30
|
every { AuthManager.getTypingEffectDelayMs() } returns 30
|
||||||
every { AuthManager.isAutoReconnect() } returns false
|
every { AuthManager.isAutoReconnect() } returns false
|
||||||
|
every { AuthManager.isRestoreLastSession() } returns false
|
||||||
|
every { AuthManager.getLastOpenedSessionId() } returns null
|
||||||
|
every { AuthManager.setLastOpenedSessionId(any()) } returns Unit
|
||||||
|
every { AuthManager.clearLastOpenedSessionId() } returns Unit
|
||||||
every { HermesWsClient.events } returns mockEventsFlow
|
every { HermesWsClient.events } returns mockEventsFlow
|
||||||
every { HermesWsClient.connectionStatus } returns mockConnectionStatus
|
every { HermesWsClient.connectionStatus } returns mockConnectionStatus
|
||||||
every { HermesWsClient.connect() } answers {
|
every { HermesWsClient.connect() } answers {
|
||||||
@@ -5083,4 +5087,75 @@ class ChatViewModelTest {
|
|||||||
assertTrue(updated.logs.any { it.text.contains("Stopped subagent") })
|
assertTrue(updated.logs.any { it.text.contains("Stopped subagent") })
|
||||||
io.mockk.verify { HermesWsClient.sendRedirect(sessionId, "/stop sub-123", any()) }
|
io.mockk.verify { HermesWsClient.sendRedirect(sessionId, "/stop sub-123", any()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testHandleGatewayReady_withRestoreLastSessionEnabled_resumesStoredSession() =
|
||||||
|
runTest {
|
||||||
|
every { AuthManager.isRestoreLastSession() } returns true
|
||||||
|
every { AuthManager.getLastOpenedSessionId() } returns "session-stored-999"
|
||||||
|
|
||||||
|
val resumeParamsSlot = slot<Map<String, Any>>()
|
||||||
|
every {
|
||||||
|
HermesWsClient.send(WsMethods.SESSION_RESUME, capture(resumeParamsSlot), any())
|
||||||
|
} answers {
|
||||||
|
"req-resume-test"
|
||||||
|
}
|
||||||
|
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
mockConnectionStatus.value = ConnectionStatus.CONNECTED
|
||||||
|
mockEventsFlow.emit(WsEvent.GatewayReady(null))
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
io.mockk.verify {
|
||||||
|
HermesWsClient.send(
|
||||||
|
WsMethods.SESSION_RESUME,
|
||||||
|
any(),
|
||||||
|
any(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
assertEquals("session-stored-999", resumeParamsSlot.captured["session_id"])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testHandleGatewayReady_withRestoreLastSessionDisabled_createsNewSession() =
|
||||||
|
runTest {
|
||||||
|
every { AuthManager.isRestoreLastSession() } returns false
|
||||||
|
every { AuthManager.getLastOpenedSessionId() } returns "session-stored-999"
|
||||||
|
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
mockConnectionStatus.value = ConnectionStatus.CONNECTED
|
||||||
|
mockEventsFlow.emit(WsEvent.GatewayReady(null))
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
io.mockk.verify {
|
||||||
|
HermesWsClient.send(
|
||||||
|
WsMethods.SESSION_CREATE,
|
||||||
|
any(),
|
||||||
|
any(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
io.mockk.verify(exactly = 0) {
|
||||||
|
HermesWsClient.send(
|
||||||
|
WsMethods.SESSION_RESUME,
|
||||||
|
any(),
|
||||||
|
any(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testSwitchSession_updatesLastOpenedSessionId() =
|
||||||
|
runTest {
|
||||||
|
val (viewModel, _) = createViewModelWithSession()
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
viewModel.switchSession("session-other-456")
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
io.mockk.verify { AuthManager.setLastOpenedSessionId("session-other-456") }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,6 +95,10 @@ class SlashCommandDispatchRpcTest {
|
|||||||
every { AuthManager.isTypingEffectEnabled() } returns true
|
every { AuthManager.isTypingEffectEnabled() } returns true
|
||||||
every { AuthManager.getTypingEffectDelayMs() } returns 30
|
every { AuthManager.getTypingEffectDelayMs() } returns 30
|
||||||
every { AuthManager.isAutoReconnect() } returns false
|
every { AuthManager.isAutoReconnect() } returns false
|
||||||
|
every { AuthManager.isRestoreLastSession() } returns false
|
||||||
|
every { AuthManager.getLastOpenedSessionId() } returns null
|
||||||
|
every { AuthManager.setLastOpenedSessionId(any()) } returns Unit
|
||||||
|
every { AuthManager.clearLastOpenedSessionId() } returns Unit
|
||||||
every { HermesWsClient.events } returns mockEventsFlow
|
every { HermesWsClient.events } returns mockEventsFlow
|
||||||
every { HermesWsClient.connectionStatus } returns mockConnectionStatus
|
every { HermesWsClient.connectionStatus } returns mockConnectionStatus
|
||||||
every { HermesWsClient.connect() } answers {
|
every { HermesWsClient.connect() } answers {
|
||||||
|
|||||||
@@ -63,6 +63,10 @@ class SettingsViewModelTest {
|
|||||||
every { AuthManager.getAppLanguage() } returns "system"
|
every { AuthManager.getAppLanguage() } returns "system"
|
||||||
every { AuthManager.getToken() } returns ""
|
every { AuthManager.getToken() } returns ""
|
||||||
every { AuthManager.isAutoReconnect() } returns true
|
every { AuthManager.isAutoReconnect() } returns true
|
||||||
|
every { AuthManager.isRestoreLastSession() } returns false
|
||||||
|
every { AuthManager.setRestoreLastSession(any()) } returns Unit
|
||||||
|
every { AuthManager.clearLastOpenedSessionIdsForConnection(any()) } returns Unit
|
||||||
|
every { AuthManager.clearLastOpenedSessionId() } returns Unit
|
||||||
every { AuthManager.getThemePreference() } returns ThemePreference.SYSTEM
|
every { AuthManager.getThemePreference() } returns ThemePreference.SYSTEM
|
||||||
every { AuthManager.isUseDynamicColors() } returns true
|
every { AuthManager.isUseDynamicColors() } returns true
|
||||||
every { AuthManager.getThemePreset() } returns ThemePreset.DEFAULT
|
every { AuthManager.getThemePreset() } returns ThemePreset.DEFAULT
|
||||||
@@ -253,4 +257,26 @@ class SettingsViewModelTest {
|
|||||||
verify { AuthManager.setSelectedProfileId(any()) }
|
verify { AuthManager.setSelectedProfileId(any()) }
|
||||||
assertEquals(true, viewModel.uiState.value.navigateToLogin)
|
assertEquals(true, viewModel.uiState.value.navigateToLogin)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testRestoreLastSession_loadsAndUpdatesState() {
|
||||||
|
every { AuthManager.isRestoreLastSession() } returns true
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
|
||||||
|
assertEquals(true, viewModel.uiState.value.restoreLastSession)
|
||||||
|
|
||||||
|
viewModel.onRestoreLastSessionChange(false)
|
||||||
|
assertEquals(false, viewModel.uiState.value.restoreLastSession)
|
||||||
|
verify { AuthManager.setRestoreLastSession(false) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDeleteProfile_clearsStoredSessionIdsForConnection() {
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
|
||||||
|
viewModel.deleteProfile("prof-1")
|
||||||
|
testDispatcher.scheduler.advanceUntilIdle()
|
||||||
|
|
||||||
|
verify { AuthManager.clearLastOpenedSessionIdsForConnection("prof-1") }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user