feat(chat): support /undo command with transcript reload and composer prefill (#1051)

This commit is contained in:
M57 authored and GitHub committed 2026-09-03 19:04:43 +03:00
1 parent efe6fe81cb
commit b3439d87c9
8 files changed
+159

No files matched your search

@@ -896,6 +896,45 @@ class ChatViewModelTest {
assertEquals("openai/gpt-4o", viewModel.uiState.value.currentSessionModel)
}
@Test
fun testUndoCommand_dispatchesAndPrefillsComposer() =
runTest {
val (viewModel, sessionId) = createViewModelWithSession()
every {
HermesWsClient.request(
WsMethods.COMMAND_DISPATCH,
mapOf("name" to "undo", "arg" to "2", "session_id" to sessionId),
any(),
)
} returns
CompletableDeferred(
mapOf(
"type" to "prefill",
"message" to "undone user message",
"notice" to "↶ Undid 2 turns",
),
)
viewModel.sendMessage("/undo 2")
advanceUntilIdle()
// Prefill text is staged in UI state
assertEquals("undone user message", viewModel.uiState.value.pendingPrefillText)
// Notice is displayed as system message with rewind target feedback
assertTrue(
viewModel.uiState.value.messages
.any { it.content.contains("↶ Undid 2 turns") && it.content.contains("undone user message") },
)
// Verify slash usage was recorded
assertEquals(1, viewModel.uiState.value.slashUsageCounts["/undo"])
// Consume prefill text
viewModel.consumePendingPrefill()
advanceUntilIdle()
assertNull(viewModel.uiState.value.pendingPrefillText)
}
// ── Connection / init tests ──────────────────────────────────────────────
@Test
@@ -82,6 +82,14 @@ class SlashCommandDispatcherTest {
assertEquals(SlashResult.SideQuestion(""), dispatcher.dispatch("/btw "))
}
@Test
fun `undo routes to Undo`() {
assertEquals(SlashResult.Undo(""), dispatcher.dispatch("/undo"))
assertEquals(SlashResult.Undo("2"), dispatcher.dispatch("/undo 2"))
assertEquals(SlashResult.Undo("3"), dispatcher.dispatch("/UNDO 3"))
assertEquals(SlashResult.Undo(""), dispatcher.dispatch("/Undo"))
}
@Test
fun `NEW uppercase still routes to NewSession`() {
// Dispatcher lower-cases before matching, so case must not matter.
@@ -27,6 +27,10 @@ class FakeChatMessageDao : ChatMessageDao {
messageList.forEach { messages[it.id] = it }
}
override suspend fun deleteMessagesForSession(sessionId: String) {
messages.values.removeAll { it.sessionId == sessionId }
}
/** Direct access for test setup — bypasses the suspend modifier. */
fun addMessageDirect(message: ChatMessageEntity) {
messages[message.id] = message