Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ android {
abortOnError = true
checkReleaseBuilds = true
warningsAsErrors = true
disable += "GradleDependency"
baseline = file("lint-baseline.xml")
}
}
Expand Down
39 changes: 34 additions & 5 deletions app/src/main/java/com/ayagmar/pimobile/chat/ChatViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1631,11 +1631,33 @@ class ChatViewModel(
null
}

val shouldForceRuntimeReload =
reason == TimelineReloadReason.MANUAL_SYNC ||
reason == TimelineReloadReason.AUTO_FRESHNESS_REFRESH

initialLoadJob?.cancel()
initialLoadJob =
viewModelScope.launch(Dispatchers.IO) {
val messagesResult = sessionController.getMessages()
val stateResult = sessionController.getState()
val reloadResult =
if (shouldForceRuntimeReload) {
sessionController.reloadActiveSessionFromDisk()
} else {
null
}
val reloadError = reloadResult?.exceptionOrNull()

val messagesResult =
if (reloadError == null) {
sessionController.getMessages()
} else {
Result.failure(reloadError)
}
val stateResult =
if (reloadError == null) {
sessionController.getState()
} else {
Result.failure(reloadError)
}

if (messagesResult.isSuccess) {
recordMetricsSafely { PerformanceMetrics.recordFirstMessagesRendered() }
Expand Down Expand Up @@ -1670,7 +1692,7 @@ class ChatViewModel(
}
}

latestSessionPath = metadata.sessionPath ?: latestSessionPath
latestSessionPath = metadata.sessionPath ?: reloadResult?.getOrNull() ?: latestSessionPath
refreshSessionFreshness(trigger = FreshnessCheckTrigger.POST_LOAD)

val refreshedHistorySignature = historyWindowSignature(historyWindowMessages)
Expand All @@ -1684,7 +1706,7 @@ class ChatViewModel(
state.copy(
isSyncingSession = false,
sessionCoherencyWarning =
if (hasPotentialExternalChanges) {
if (messagesResult.isFailure || hasPotentialExternalChanges) {
SESSION_COHERENCY_WARNING_MESSAGE
} else {
null
Expand All @@ -1705,7 +1727,14 @@ class ChatViewModel(
}
} else if (reason == TimelineReloadReason.AUTO_FRESHNESS_REFRESH) {
_uiState.update {
it.copy(sessionCoherencyWarning = null)
it.copy(
sessionCoherencyWarning =
if (messagesResult.isSuccess) {
null
} else {
SESSION_COHERENCY_WARNING_MESSAGE
},
)
}

if (messagesResult.isSuccess) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,30 @@ class RpcSessionController(
}
}

override suspend fun reloadActiveSessionFromDisk(): Result<String?> {
return mutex.withLock {
runCatching {
val connection = ensureActiveConnection()
val sessionPath = refreshCurrentSessionPath(connection)
check(!sessionPath.isNullOrBlank()) {
"No active session file available to reload"
}

val switchResponse =
sendAndAwaitResponse(
connection = connection,
requestTimeoutMs = requestTimeoutMs,
command = SwitchSessionCommand(id = UUID.randomUUID().toString(), sessionPath = sessionPath),
expectedCommand = SWITCH_SESSION_COMMAND,
).requireSuccess("Failed to reload active session")

switchResponse.requireNotCancelled("Active session reload was cancelled")

refreshCurrentSessionPath(connection)
}
}
}

override suspend fun renameSession(name: String): Result<String?> {
return mutex.withLock {
runCatching {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ interface SessionController {

suspend fun getState(): Result<RpcResponse>

suspend fun reloadActiveSessionFromDisk(): Result<String?>

suspend fun sendPrompt(
message: String,
images: List<ImagePayload> = emptyList(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,8 +254,10 @@ class SessionsViewModel(
}

fun resumeSession(session: SessionRecord) {
val hostId = _uiState.value.selectedHostId ?: return
val selectedHost = _uiState.value.hosts.firstOrNull { host -> host.id == hostId } ?: return
val state = _uiState.value
val hostId = state.selectedHostId ?: return
val selectedHost = state.hosts.firstOrNull { host -> host.id == hostId } ?: return
val isOpenCurrentSession = state.activeSessionPath == session.sessionPath

// Record resume start for performance tracking
PerformanceMetrics.recordResumeStart()
Expand Down Expand Up @@ -291,7 +293,8 @@ class SessionsViewModel(

if (resumeResult.isSuccess) {
markConnectionWarm(hostId = hostId, cwd = session.cwd)
emitMessage("Resumed ${session.summaryTitle()}")
val action = if (isOpenCurrentSession) "Opened" else "Resumed"
emitMessage("$action ${session.summaryTitle()}")
_navigateToChat.trySend(Unit)
}

Expand Down
11 changes: 8 additions & 3 deletions app/src/main/java/com/ayagmar/pimobile/ui/components/PiTopBar.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.ayagmar.pimobile.ui.components

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
Expand All @@ -15,10 +15,15 @@ fun PiTopBar(
) {
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
title()
Box(
modifier = Modifier.weight(1f),
contentAlignment = Alignment.CenterStart,
) {
title()
}

actions()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -610,8 +610,8 @@ private fun SessionCardFooter(
)

PiButton(
label = if (isActive) "Active" else "Resume",
enabled = !isBusy && !isActive,
label = if (isActive) "Open" else "Resume",
enabled = !isBusy,
onClick = onResumeClick,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -748,6 +748,7 @@ class ChatViewModelThinkingExpansionTest {

waitForState(viewModel) { state -> !state.isSyncingSession }
val state = viewModel.uiState.value
assertEquals(1, controller.reloadActiveSessionCallCount)
assertEquals(
"Potential cross-device session edits detected. Use Sync now before continuing.",
state.sessionCoherencyWarning,
Expand All @@ -773,9 +774,37 @@ class ChatViewModelThinkingExpansionTest {
dispatcher.scheduler.advanceUntilIdle()

waitForState(viewModel) { state -> !state.isSyncingSession }
assertEquals(2, controller.reloadActiveSessionCallCount)
assertEquals(null, viewModel.uiState.value.sessionCoherencyWarning)
}

@Test
fun syncNowShowsErrorAndSkipsMessageFetchWhenReloadFails() =
runTest(dispatcher) {
val controller = FakeSessionController()
controller.messagesPayload = historyWithMessageTexts(listOf("unchanged"))
val viewModel = createViewModel(controller)
dispatcher.scheduler.advanceUntilIdle()
awaitInitialLoad(viewModel)

val baselineMessageCalls = controller.getMessagesCallCount
val baselineStateCalls = controller.getStateCallCount
controller.reloadActiveSessionResult = Result.failure(IllegalStateException("reload failed"))

viewModel.syncNow()
dispatcher.scheduler.advanceUntilIdle()

waitForState(viewModel) { state -> !state.isSyncingSession }
assertEquals(1, controller.reloadActiveSessionCallCount)
assertEquals(baselineMessageCalls, controller.getMessagesCallCount)
assertEquals(baselineStateCalls, controller.getStateCallCount)
assertEquals("reload failed", viewModel.uiState.value.errorMessage)
assertEquals(
"Potential cross-device session edits detected. Use Sync now before continuing.",
viewModel.uiState.value.sessionCoherencyWarning,
)
}

@Test
fun jumpAndContinueUsesInPlaceTreeNavigationResult() =
runTest(dispatcher) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class FakeSessionController : SessionController {
var sendPromptCallCount: Int = 0
var getMessagesCallCount: Int = 0
var getStateCallCount: Int = 0
var reloadActiveSessionCallCount: Int = 0
var getSessionFreshnessCallCount: Int = 0
var getStateResult: Result<RpcResponse> =
Result.success(
Expand All @@ -47,6 +48,7 @@ class FakeSessionController : SessionController {
success = true,
),
)
var reloadActiveSessionResult: Result<String?> = Result.success(null)
var lastPromptMessage: String? = null
var lastFreshnessSessionPath: String? = null
var lastImportedSessionFileName: String? = null
Expand Down Expand Up @@ -147,6 +149,11 @@ class FakeSessionController : SessionController {
return getStateResult
}

override suspend fun reloadActiveSessionFromDisk(): Result<String?> {
reloadActiveSessionCallCount += 1
return reloadActiveSessionResult
}

override suspend fun sendPrompt(
message: String,
images: List<ImagePayload>,
Expand Down
Loading