diff --git a/app/src/test/java/com/ayagmar/pimobile/chat/ChatViewModelWorkflowCommandTest.kt b/app/src/test/java/com/ayagmar/pimobile/chat/ChatViewModelWorkflowCommandTest.kt index c23e1ea..afc8713 100644 --- a/app/src/test/java/com/ayagmar/pimobile/chat/ChatViewModelWorkflowCommandTest.kt +++ b/app/src/test/java/com/ayagmar/pimobile/chat/ChatViewModelWorkflowCommandTest.kt @@ -1,10 +1,12 @@ package com.ayagmar.pimobile.chat +import androidx.lifecycle.viewModelScope import com.ayagmar.pimobile.corerpc.ExtensionUiRequestEvent import com.ayagmar.pimobile.sessions.SlashCommandInfo import com.ayagmar.pimobile.testutil.FakeSessionController import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancel import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest @@ -20,17 +22,25 @@ import org.junit.Test @OptIn(ExperimentalCoroutinesApi::class) class ChatViewModelWorkflowCommandTest { private val dispatcher = StandardTestDispatcher() + private val viewModels = mutableListOf() @Before fun setUp() { Dispatchers.setMain(dispatcher) + viewModels.clear() } @After fun tearDown() { + viewModels.forEach { it.viewModelScope.cancel() } + viewModels.clear() Dispatchers.resetMain() } + private fun createViewModel(controller: FakeSessionController): ChatViewModel { + return ChatViewModel(sessionController = controller).also { viewModels.add(it) } + } + @Test fun loadingCommandsHidesInternalBridgeWorkflowCommands() = runTest(dispatcher) { @@ -60,7 +70,7 @@ class ChatViewModelWorkflowCommandTest { ), ) - val viewModel = ChatViewModel(sessionController = controller) + val viewModel = createViewModel(controller) dispatcher.scheduler.advanceUntilIdle() awaitInitialLoad(viewModel) @@ -88,7 +98,7 @@ class ChatViewModelWorkflowCommandTest { ), ) - val viewModel = ChatViewModel(sessionController = controller) + val viewModel = createViewModel(controller) dispatcher.scheduler.advanceUntilIdle() awaitInitialLoad(viewModel) @@ -113,7 +123,7 @@ class ChatViewModelWorkflowCommandTest { fun selectingBridgeBackedBuiltinStatsFallsBackWhenInternalCommandUnavailable() = runTest(dispatcher) { val controller = FakeSessionController() - val viewModel = ChatViewModel(sessionController = controller) + val viewModel = createViewModel(controller) dispatcher.scheduler.advanceUntilIdle() awaitInitialLoad(viewModel) @@ -137,7 +147,7 @@ class ChatViewModelWorkflowCommandTest { fun internalWorkflowStatusActionCanOpenStatsSheet() = runTest(dispatcher) { val controller = FakeSessionController() - val viewModel = ChatViewModel(sessionController = controller) + val viewModel = createViewModel(controller) dispatcher.scheduler.advanceUntilIdle() awaitInitialLoad(viewModel) @@ -160,7 +170,7 @@ class ChatViewModelWorkflowCommandTest { fun nonWorkflowStatusIsStoredUpdatedAndCleared() = runTest(dispatcher) { val controller = FakeSessionController() - val viewModel = ChatViewModel(sessionController = controller) + val viewModel = createViewModel(controller) dispatcher.scheduler.advanceUntilIdle() awaitInitialLoad(viewModel) diff --git a/app/src/test/java/com/ayagmar/pimobile/ui/settings/SettingsViewModelTest.kt b/app/src/test/java/com/ayagmar/pimobile/ui/settings/SettingsViewModelTest.kt index a579e70..728b834 100644 --- a/app/src/test/java/com/ayagmar/pimobile/ui/settings/SettingsViewModelTest.kt +++ b/app/src/test/java/com/ayagmar/pimobile/ui/settings/SettingsViewModelTest.kt @@ -3,11 +3,13 @@ package com.ayagmar.pimobile.ui.settings import android.content.SharedPreferences +import androidx.lifecycle.viewModelScope import com.ayagmar.pimobile.sessions.TransportPreference import com.ayagmar.pimobile.testutil.FakeSessionController import com.ayagmar.pimobile.ui.theme.ThemePreference import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.resetMain @@ -23,14 +25,18 @@ import org.junit.Test @OptIn(ExperimentalCoroutinesApi::class) class SettingsViewModelTest { private val dispatcher = StandardTestDispatcher() + private val viewModels = mutableListOf() @Before fun setUp() { Dispatchers.setMain(dispatcher) + viewModels.clear() } @After fun tearDown() { + viewModels.forEach { it.viewModelScope.cancel() } + viewModels.clear() Dispatchers.resetMain() } @@ -133,7 +139,7 @@ class SettingsViewModelTest { sessionController = controller, sharedPreferences = InMemorySharedPreferences(), appVersionOverride = "test", - ) + ).also { viewModels.add(it) } } } diff --git a/docs/ai/pi-mobile-ux-resume-fix-plan.md b/docs/ai/pi-mobile-ux-resume-fix-plan.md deleted file mode 100644 index 7bf7657..0000000 --- a/docs/ai/pi-mobile-ux-resume-fix-plan.md +++ /dev/null @@ -1,72 +0,0 @@ -# Pi Mobile UX and Resume Fix Plan - -## Issues Identified from Screenshot and Logs - -### 1. Duplicate User Messages (HIGH PRIORITY) -**Problem:** User messages appear twice - once from optimistic insertion in `sendPrompt()` and again from `MessageEndEvent`. - -**Fix:** Remove optimistic insertion, only add user messages from `MessageEndEvent`. This ensures single source of truth. - -### 2. Resume Not Working After First Resume (HIGH PRIORITY) -**Problem:** When user switches sessions, the `switch_session` command succeeds but ChatViewModel doesn't reload the timeline. - -**Root Cause:** `RpcSessionController.resume()` returns success, but there's no mechanism to notify ChatViewModel that the session changed. The response goes to `sendAndAwaitResponse()` but isn't broadcast to observers. - -**Fix Options:** -- Option A: Have SessionController emit a "sessionChanged" event that ChatViewModel observes -- Option B: Have ChatViewModel poll for session path changes -- Option C: Use SharedFlow to broadcast switch_session success - -**Chosen:** Option A - Add `sessionChanged` SharedFlow to SessionController that emits when session successfully switches. - -### 3. UI Clutter (MEDIUM PRIORITY) -**Problems:** -- Too many buttons in top bar (Tree, stats, copy, export) -- Collapse/expand all buttons add more clutter -- Weird status text at bottom ("3 pkgs • ... weekly • 1 update...") -- Model selector and thinking dropdown take too much space - -**Fix:** -- Move Tree, stats, copy, export to overflow menu or bottom sheet -- Remove collapse/expand all from main UI (keep in overflow menu) -- Fix or remove the bottom status text -- Simplify model/thinking display - -### 4. Message Alignment Already Working -**Status:** User messages ARE on the right ("You" cards), assistant on left. This is correct. - -## Implementation Order - -1. Fix duplicate messages (remove optimistic insertion) -2. Fix resume by adding session change notification -3. Clean up UI clutter - -## Architecture for Resume Fix - -```kotlin -// SessionController interface -interface SessionController { - // ... existing methods ... - - // New: Observable session changes - val sessionChanged: SharedFlow // emits new session path or null -} - -// RpcSessionController implementation -override suspend fun resume(...): Result { - // ... existing logic ... - - if (success) { - _sessionChanged.emit(newSessionPath) - } -} - -// ChatViewModel -init { - viewModelScope.launch { - sessionController.sessionChanged.collect { newPath - - loadInitialMessages() // Reload timeline - } - } -} -```