From e191e9c6f2c3cc75949aec80f0bb0c8199a2e9b5 Mon Sep 17 00:00:00 2001 From: Hot Pixel Group Date: Sun, 5 Jul 2026 05:15:57 -0700 Subject: [PATCH] Fix slash-command suggestions that insert commands the processor rejects (#1354) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The autocomplete panel is the only in-app surface for discovering slash commands, but several suggestions do not match what CommandProcessor accepts, so tapping them inserts a command that returns "unknown command": - CommandInfo suggests /dm, /favorite, /unfavorite, but the processor only handles /m, /msg, /fav, /unfav. Aliases are aligned to the accepted spellings (msg, fav, unfav). - Favorites are suggested only in geohash contexts (isGeoPublic || isGeoDM) — exactly where the processor rejects them ("favorites are only for mesh peers"). The gating is inverted so they appear in mesh, where they work. Also, small related fixes to the discovery surface: - /help is now handled (the ChatViewModel command docstring already claimed it existed); it prints a local system line listing the valid commands, and the unknown-command error points at it. - The suggestion panel keeps the matched command's usage row (e.g. "/msg ") visible while arguments are typed, instead of vanishing at the first space; in that mode the row is informational and no longer overwrites the draft on tap. New string is added source-language (en) only. The CommandInfo contract test is updated to the corrected metadata. --- bitchat/Localizable.xcstrings | 12 +++++++ bitchat/Models/CommandInfo.swift | 31 ++++++++++++------- bitchat/Services/CommandProcessor.swift | 19 +++++++++++- .../Components/CommandSuggestionsView.swift | 24 ++++++++++++-- bitchatTests/ProtocolContractTests.swift | 15 ++++++--- 5 files changed, 80 insertions(+), 21 deletions(-) diff --git a/bitchat/Localizable.xcstrings b/bitchat/Localizable.xcstrings index be1bba06..d7c842cd 100644 --- a/bitchat/Localizable.xcstrings +++ b/bitchat/Localizable.xcstrings @@ -14707,6 +14707,18 @@ } } }, + "content.commands.help" : { + "comment" : "Description of the /help command in the suggestions panel", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "show available commands" + } + } + } + }, "content.commands.hug" : { "extractionState" : "manual", "localizations" : { diff --git a/bitchat/Models/CommandInfo.swift b/bitchat/Models/CommandInfo.swift index 0bf25537..1f8245be 100644 --- a/bitchat/Models/CommandInfo.swift +++ b/bitchat/Models/CommandInfo.swift @@ -11,33 +11,38 @@ import Foundation // MARK: - CommandInfo Enum enum CommandInfo: String, Identifiable { + // Raw values must match the aliases CommandProcessor actually accepts — + // the suggestion panel is the app's only command-discovery surface, and + // suggesting a spelling the processor rejects teaches users dead ends. case block case clear + case help case hug - case message = "dm" + case message = "msg" case slap case unblock case who - case favorite - case unfavorite - + case favorite = "fav" + case unfavorite = "unfav" + var id: String { rawValue } - + var alias: String { "/" + rawValue } - + var placeholder: String? { switch self { case .block, .hug, .message, .slap, .unblock, .favorite, .unfavorite: return "<" + String(localized: "content.input.nickname_placeholder") + ">" - case .clear, .who: + case .clear, .help, .who: return nil } } - + var description: String { switch self { case .block: String(localized: "content.commands.block") case .clear: String(localized: "content.commands.clear") + case .help: String(localized: "content.commands.help") case .hug: String(localized: "content.commands.hug") case .message: String(localized: "content.commands.message") case .slap: String(localized: "content.commands.slap") @@ -47,12 +52,14 @@ enum CommandInfo: String, Identifiable { case .unfavorite: String(localized: "content.commands.unfavorite") } } - + static func all(isGeoPublic: Bool, isGeoDM: Bool) -> [CommandInfo] { - let baseCommands: [CommandInfo] = [.block, .unblock, .clear, .hug, .message, .slap, .who] + let baseCommands: [CommandInfo] = [.block, .unblock, .clear, .help, .hug, .message, .slap, .who] + // The processor rejects favorites in geohash contexts, so only + // suggest them where they actually work: mesh. if isGeoPublic || isGeoDM { - return baseCommands + [.favorite, .unfavorite] + return baseCommands } - return baseCommands + return baseCommands + [.favorite, .unfavorite] } } diff --git a/bitchat/Services/CommandProcessor.swift b/bitchat/Services/CommandProcessor.swift index e9e81654..680c20f8 100644 --- a/bitchat/Services/CommandProcessor.swift +++ b/bitchat/Services/CommandProcessor.swift @@ -105,11 +105,28 @@ final class CommandProcessor { case "/unfav": if inGeoPublic || inGeoDM { return .error(message: "favorites are only for mesh peers in #mesh") } return handleFavorite(args, add: false) + case "/help": + return .success(message: Self.helpText) default: - return .error(message: "unknown command: \(cmd)") + return .error(message: "unknown command: \(cmd) — type /help for commands") } } + /// Local-only command reference, printed as a system message. The + /// suggestion panel hides once arguments are typed, and typos used to + /// dead-end in a bare "unknown command" — this is the way out. + static let helpText = """ + commands: + /msg @name [message] — start a private chat + /who — list who's here + /clear — clear this chat + /hug @name — send a hug + /slap @name — slap with a large trout + /block @name · /unblock @name + /fav @name · /unfav @name — favorites (mesh only) + /help — this list + """ + // MARK: - Command Handlers private func handleMessage(_ args: String) -> CommandResult { diff --git a/bitchat/Views/Components/CommandSuggestionsView.swift b/bitchat/Views/Components/CommandSuggestionsView.swift index 786a3caf..ca357771 100644 --- a/bitchat/Views/Components/CommandSuggestionsView.swift +++ b/bitchat/Views/Components/CommandSuggestionsView.swift @@ -14,23 +14,41 @@ struct CommandSuggestionsView: View { @Binding var messageText: String + /// The command already typed in full, once arguments have begun. + private var typedCommandAlias: String? { + guard messageText.hasPrefix("/"), + let spaceIndex = messageText.firstIndex(of: " ") + else { return nil } + return String(messageText[..