Skip to content

Commit e552ee0

Browse files
committed
1.5.2: popup identity fix + bug sweep
Slash popup was caching rows under conflicting identity signals (ForEach element.id and .id(idx)); unified on positional identity so rows rebuild when the matches list changes. Removed the 8-item prefix cap so typing just "/" shows the full command list. Toast auto-dismiss now verifies it's still the active toast by id to avoid skipping queued entries. /rewind clamps to [1, 50]. DiffSheet uses LazyVStack so huge diffs don't hitch on open.
1 parent 19d2788 commit e552ee0

5 files changed

Lines changed: 55 additions & 20 deletions

File tree

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,27 @@ All notable changes to Kiln land here. Format loosely follows
44
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Dates are
55
YYYY-MM-DD, versions follow [SemVer](https://semver.org/).
66

7+
## [1.5.2] — 2026-04-21
8+
9+
### Fixed
10+
- **Slash popup stale rendering** — conflicting identity signals
11+
(ForEach by `element.id` + row `.id(idx)`) kept old rows cached
12+
when the matches list shrank. Unified on positional identity so
13+
each row's body rebuilds with the current command.
14+
- **Slash popup now shows all commands** when the input is just `/`
15+
was capped at 8 entries; LazyVStack handles the full list fine.
16+
- **Toast auto-dismiss race** — between sleep and wake, a manual
17+
tap-to-dismiss could cause the next queued toast to be skipped.
18+
The auto-dismiss now verifies it's still the active toast by id.
19+
- **`/rewind` cap** — parse result clamped to [1, 50] so a typo
20+
like `/rewind 999` can't quietly shred a session.
21+
- **DiffSheet performance** — switched to `LazyVStack` so a 10k-line
22+
diff doesn't blow out the view hierarchy on open.
23+
24+
### Added
25+
- Toast feedback for `/rewind` — confirms how many exchanges were
26+
dropped.
27+
728
## [1.5.1] — 2026-04-21
829

930
### Fixed

Sources/Services/ToastCenter.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,13 @@ final class ToastCenter: ObservableObject {
4343
dismissTask = Task { [weak self] in
4444
try? await Task.sleep(nanoseconds: UInt64(toast.duration * 1_000_000_000))
4545
guard !Task.isCancelled else { return }
46-
await MainActor.run { self?.dismissCurrent() }
46+
await MainActor.run {
47+
// Only auto-dismiss if we're still the active toast —
48+
// otherwise a tap-to-dismiss between sleep and wake
49+
// would pop the *next* queued toast prematurely.
50+
guard self?.current?.id == toast.id else { return }
51+
self?.dismissCurrent()
52+
}
4753
}
4854
}
4955
}

Sources/Views/Chat/ChatView.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2351,7 +2351,10 @@ struct DiffSheet: View {
23512351
.padding(12)
23522352
Divider()
23532353
ScrollView {
2354-
VStack(alignment: .leading, spacing: 0) {
2354+
// LazyVStack so a 10k-line diff doesn't blow out the
2355+
// view hierarchy up front — only the visible rows get
2356+
// materialized.
2357+
LazyVStack(alignment: .leading, spacing: 0) {
23552358
ForEach(Array(content.split(separator: "\n", omittingEmptySubsequences: false).enumerated()), id: \.offset) { _, line in
23562359
Text(String(line).isEmpty ? " " : String(line))
23572360
.font(.system(size: 11, design: .monospaced))

Sources/Views/Chat/ComposerView.swift

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -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) }

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.5.1
1+
1.5.2

0 commit comments

Comments
 (0)