@@ -394,26 +394,23 @@ struct ComposerView: View {
394394 /// shouldn't pull up every command that happens to have "t" in its
395395 /// description.
396396 private var slashMatches : [ SlashCommand ] ? {
397- let trimmed = input. trimmingCharacters ( in: . whitespaces )
397+ let trimmed = input. trimmingCharacters ( in: . whitespacesAndNewlines )
398398 guard trimmed. hasPrefix ( " / " ) else { return nil }
399399 // Only fire while the input is a single word (no spaces yet).
400400 if trimmed. contains ( " " ) { return nil }
401401 let query = String ( trimmed. dropFirst ( ) ) . lowercased ( )
402402 let all = SlashCommands . all ( )
403403 if query. isEmpty { return all }
404- // Score each candidate: 0 = exact, 1 = prefix, 2 = contains,
405- // nil = not a match. Stable sort within each bucket preserves
406- // the authored order in `SlashCommands.builtins`.
407- let scored : [ ( SlashCommand , Int ) ] = all. compactMap { c in
408- let body = c. label. lowercased ( ) . dropFirst ( ) // strip the leading "/"
409- let labelLower = String ( body)
410- if labelLower == query { return ( c, 0 ) }
411- if labelLower. hasPrefix ( query) { return ( c, 1 ) }
412- if labelLower. contains ( query) { return ( c, 2 ) }
413- return nil
404+ var exact : [ SlashCommand ] = [ ]
405+ var prefix : [ SlashCommand ] = [ ]
406+ var contains : [ SlashCommand ] = [ ]
407+ for c in all {
408+ let body = String ( c. label. dropFirst ( ) ) . lowercased ( )
409+ if body == query { exact. append ( c) }
410+ else if body. hasPrefix ( query) { prefix. append ( c) }
411+ else if body. contains ( query) { contains. append ( c) }
414412 }
415- let sorted = scored. sorted { $0. 1 < $1. 1 }
416- return sorted. map { $0. 0 }
413+ return exact + prefix + contains
417414 }
418415
419416 private func insertSlashCommand( _ cmd: SlashCommand ) {
@@ -647,10 +644,13 @@ struct ComposerView: View {
647644 }
648645 case " /rewind " :
649646 // /rewind N — drop the last N message pairs from the session.
650- // Default N = 1. Non-destructive on disk until saveSession runs.
651- let n = Int ( arg. trimmingCharacters ( in: . whitespaces) ) ?? 1
652- if let id = store. activeSessionId, n > 0 {
647+ // Default N = 1. Capped at 50 so a typo can't nuke a long
648+ // session. Non-destructive on disk until saveSession runs.
649+ let parsed = Int ( arg. trimmingCharacters ( in: . whitespaces) ) ?? 1
650+ let n = max ( 1 , min ( 50 , parsed) )
651+ if let id = store. activeSessionId {
653652 store. rewindSession ( id, count: n)
653+ ToastCenter . shared. show ( " Rewound \( n) exchange \( n == 1 ? " " : " s " ) " , kind: . info)
654654 }
655655 default :
656656 // Handle dynamic template aliases like `/t:review`.
@@ -1278,7 +1278,12 @@ struct SlashCommandPopup: View {
12781278 ScrollViewReader { proxy in
12791279 ScrollView {
12801280 LazyVStack ( spacing: 1 ) {
1281- ForEach ( Array ( matches. prefix ( 8 ) . enumerated ( ) ) , id: \. element. id) { idx, cmd in
1281+ // Identity by positional index so SwiftUI rebuilds
1282+ // each row's body when `matches` changes (shrinking
1283+ // the list used to leave stale rows cached under
1284+ // `.id(idx)` while ForEach tracked element.id —
1285+ // two conflicting identities). One signal only.
1286+ ForEach ( Array ( matches. enumerated ( ) ) , id: \. offset) { idx, cmd in
12821287 SlashCommandRow ( cmd: cmd, selected: idx == selected)
12831288 . id ( idx)
12841289 . onTapGesture { onPick ( cmd) }
0 commit comments