eleven / messenger Public

ios+mac: fix /me being capped out of botful typeaheads; repair stale floor tests (#642)

The 8-item cap fell on /me because #497 appended it after the bot floor —
the web's commandCatalog lists panels and /me first, so its cap victim is
the bot list's tail. Reorder to match, and update the three floor tests
that predate the panel commands (plus pin reachability-under-prefix for
every bot command).
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 3c64c0600dfd554361c681a9371af69404f0897f parent b7c3dbe
2 files changed, +25 −10
  • ios/LChatCore/Sources/LChatCore/ComposerTypeahead.swift +7 −2
  • ios/LChatCore/Tests/LChatCoreTests/ComposerTypeaheadTests.swift +18 −8
diff --git a/ios/LChatCore/Sources/LChatCore/ComposerTypeahead.swift b/ios/LChatCore/Sources/LChatCore/ComposerTypeahead.swift
index 680ad05..4a8942f 100644
--- a/ios/LChatCore/Sources/LChatCore/ComposerTypeahead.swift
+++ b/ios/LChatCore/Sources/LChatCore/ComposerTypeahead.swift
@@ -96,8 +96,13 @@ public func typeaheadItems(_ token: ComposerToken, roster: [RosterEntry],
// theming and /me (SlashCommands.swift) while /help falls through to a
// bot — the web's mirror image — so each side ungates exactly what it
// can answer itself.
- let floor = botCommands.filter { hasBot || clientAnswered.contains($0.name) }
- + WebPanelCommands.catalog + [meCommand]
+ // Ordered like the web's commandCatalog: app verbs lead (appCommands,
+ // below), then the panel commands, then /me, then the bot floor — so
+ // the 8-item cap can only ever hide the TAIL of the bot list (the
+ // web's cap victim is /profile too), never /me, which the previous
+ // append-last ordering silently dropped in every botful thread.
+ let floor = WebPanelCommands.catalog + [meCommand]
+ + botCommands.filter { hasBot || clientAnswered.contains($0.name) }
// Deduplicate by name (case-insensitive) to avoid showing the same command twice
var seen = Set<String>()
return (appCommands + floor)
diff --git a/ios/LChatCore/Tests/LChatCoreTests/ComposerTypeaheadTests.swift b/ios/LChatCore/Tests/LChatCoreTests/ComposerTypeaheadTests.swift
index 1ba233f..830d5f6 100644
--- a/ios/LChatCore/Tests/LChatCoreTests/ComposerTypeaheadTests.swift
+++ b/ios/LChatCore/Tests/LChatCoreTests/ComposerTypeaheadTests.swift
@@ -58,18 +58,28 @@ final class ComposerTypeaheadTests: XCTestCase {
}
func testBotlessChatStillOffersClientAnsweredCommands() {
- // A DM with no AI agent must still offer what the native client answers
- // itself (SlashCommands.swift): theming and /me — and nothing that only
- // a bot could answer (/help would post into the void).
- XCTAssertEqual(commandNames(hasBot: false), ["/theme", "/themes", "/me"])
+ // A DM with no AI agent must still offer what the client answers
+ // itself: the panel commands (/emoji, /gif — the command-host webview,
+ // #497), /me, and theming (SlashCommands.swift) — and nothing that
+ // only a bot could answer (/help would post into the void). Ordered
+ // like the web's commandCatalog: panels, /me, then the bot floor.
+ XCTAssertEqual(commandNames(hasBot: false), ["/emoji", "/gif", "/me", "/theme", "/themes"])
}
func testBotfulChatOffersTheFullFloor() {
- let names = commandNames(hasBot: true)
+ // Nine commands exist with a bot present but the typeahead caps at 8
+ // (the web's cap too) — the ordering guarantees the victim is the
+ // TAIL of the bot list (/profile), never /me or a panel command.
+ XCTAssertEqual(commandNames(hasBot: true),
+ ["/emoji", "/gif", "/me", "/help", "/theme", "/themes", "/dark", "/light"])
+ // Every bot command is still reachable the moment its prefix is typed —
+ // the cap hides, it never disables.
for expected in botCommands.map(\.name) {
- XCTAssertTrue(names.contains(expected), "\(expected) missing with a bot present")
+ let q = ComposerToken(kind: .command, query: String(expected.dropFirst()), start: 0)
+ let names = typeaheadItems(q, roster: [], myUserId: "me", hasBot: true)
+ .compactMap { if case .command(let name, _) = $0 { return name } else { return nil } }
+ XCTAssertTrue(names.contains(expected), "\(expected) unreachable with a bot present")
}
- XCTAssertTrue(names.contains("/me"))
}
// ---- composerToken (ui.js) parity -------------------------------------
@@ -155,7 +165,7 @@ final class ComposerTypeaheadTests: XCTestCase {
let bare = ComposerToken(kind: .command, query: "", start: 0)
XCTAssertEqual(typeaheadItems(bare, roster: roster, myUserId: "u2", hasBot: false,
appCommands: add).map(\.insert),
- ["/add ", "/theme ", "/themes ", "/me "],
+ ["/add ", "/emoji ", "/gif ", "/me ", "/theme ", "/themes "],
"no bot → the local verb leads; client-answered floor follows")
XCTAssertEqual(typeaheadItems(bare, roster: roster, myUserId: "u2", hasBot: true,
appCommands: add).map(\.insert).first,