Route command output to the conversation where the command was typed (#1363)

CommandProcessor results (/help text, errors like "unknown command",
/msg confirmations) were always appended to the public timeline via
addSystemMessage, so a command typed inside a DM appeared to do
nothing until the user switched back to the public channel.

handleCommand now routes .success/.error output to the open private
chat when one is selected, falling back to the public timeline
otherwise. The DM selection is read after processing so commands that
switch chats (/msg) print into the conversation they just opened.

Follow-up to #1354, which added /help and surfaced this routing gap.

Co-authored-by: jack <jackjackbits@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jack
2026-07-05 20:26:31 +02:00
committed by GitHub
co-authored by jack Claude Fable 5
parent e191e9c6f2
commit 66536063ca
2 changed files with 69 additions and 3 deletions
+16 -3
View File
@@ -1435,7 +1435,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, TransportEventDele
/// Processes IRC-style commands starting with '/'.
/// - Parameter command: The full command string including the leading slash
/// - Note: Supports commands like /nick, /msg, /who, /slap, /clear, /help
/// - Note: Supports commands like /msg, /who, /slap, /clear, /help
@MainActor
func handleCommand(_ command: String) {
let result = commandProcessor.process(command)
@@ -1443,16 +1443,29 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, TransportEventDele
switch result {
case .success(let message):
if let msg = message {
addSystemMessage(msg)
addCommandOutput(msg)
}
case .error(let message):
addSystemMessage(message)
addCommandOutput(message)
case .handled:
// Command was handled, no message needed
break
}
}
/// Command output belongs in the conversation where the user typed the
/// command; the public timeline is invisible while a DM is open. The DM
/// selection is read *after* processing so commands that switch chats
/// (`/msg`) print into the conversation they just opened.
@MainActor
private func addCommandOutput(_ content: String) {
if let peerID = selectedPrivateChatPeer {
addLocalPrivateSystemMessage(content, to: peerID)
} else {
addSystemMessage(content)
}
}
// MARK: - Message Reception
@MainActor
+53
View File
@@ -263,6 +263,59 @@ struct ChatViewModelCommandTests {
#expect(transport.sentPrivateMessages.isEmpty)
}
}
@Test @MainActor
func handleCommand_outputRoutesToOpenPrivateChat() async {
let (viewModel, transport) = makeTestableViewModel()
let peerID = PeerID(str: "0000000000000002")
transport.simulateConnect(peerID, nickname: "Alice")
viewModel.selectedPrivateChatPeer = peerID
viewModel.handleCommand("/help")
#expect(viewModel.privateChats[peerID]?.last?.content == CommandProcessor.helpText)
#expect(!viewModel.messages.contains { $0.content == CommandProcessor.helpText })
}
@Test @MainActor
func handleCommand_errorRoutesToOpenPrivateChat() async {
let (viewModel, transport) = makeTestableViewModel()
let peerID = PeerID(str: "0000000000000002")
transport.simulateConnect(peerID, nickname: "Alice")
viewModel.selectedPrivateChatPeer = peerID
viewModel.handleCommand("/bogus")
let dmContents = viewModel.privateChats[peerID]?.map(\.content) ?? []
#expect(dmContents.contains { $0.hasPrefix("unknown command: /bogus") })
#expect(!viewModel.messages.contains { $0.content.hasPrefix("unknown command: /bogus") })
}
@Test @MainActor
func handleCommand_outputRoutesToPublicTimelineWithoutOpenDM() async {
let (viewModel, _) = makeTestableViewModel()
viewModel.handleCommand("/bogus")
#expect(viewModel.messages.last?.content.hasPrefix("unknown command: /bogus") == true)
}
@Test @MainActor
func handleCommand_msgSuccessLandsInNewlyOpenedChat() async {
let (viewModel, transport) = makeTestableViewModel()
let peerID = PeerID(str: "0000000000000002")
transport.simulateConnect(peerID, nickname: "Alice")
let resolved = await TestHelpers.waitUntil({
viewModel.getPeerIDForNickname("Alice") == peerID
}, timeout: TestConstants.defaultTimeout)
#expect(resolved)
viewModel.handleCommand("/msg Alice")
#expect(viewModel.selectedPrivateChatPeer == peerID)
#expect(viewModel.privateChats[peerID]?.last?.content == "started private chat with Alice")
#expect(!viewModel.messages.contains { $0.content == "started private chat with Alice" })
}
}
// MARK: - Composer Tests