@@ -315,7 +315,23 @@ struct LogTableView: View {
315315 }
316316 .onChange(of: sortColumn) { _, _ in debouncedRebuildSort() }
317317 .onChange(of: sortAscending) { _, _ in debouncedRebuildSort() }
318+ .onChange(of: selectedRecord) { _, newValue in
319+ // The owner can drop the selection on its own (QueryViewModel
320+ // clears it after every query). When the new result set equals the
321+ // old one, `onChange(of: records)` doesn't fire, so nothing would
322+ // reconcile the row highlight: the row stayed highlighted with no
323+ // detail pane, and the next click on it merely "deselected" it.
324+ if newValue == nil { selectedIndex = nil }
325+ }
318326 .onAppear {
327+ // This branch is unmounted while results are empty (e.g. across a
328+ // stream switch, which clears them first), so `onChange(of: columns)`
329+ // never sees the switch. Drop a sort column the new columns don't
330+ // have — or hide — before sorting, or rows would be ordered by an
331+ // invisible column with no header showing the sort.
332+ if let sortColumn, !columns.contains(sortColumn) {
333+ self.sortColumn = nil
334+ }
319335 recomputeColumnWidths(columns: columns, records: records)
320336 rebuildSort()
321337 }
@@ -401,6 +417,9 @@ struct LogTableView: View {
401417 }
402418
403419 private func rebuildSort() {
420+ // A debounced rebuild scheduled before the table unmounted must not land
421+ // after (and overwrite) this synchronous one.
422+ sortTask?.cancel()
404423 let sevCols = buildSeverityColumnSet(columns: columns)
405424 let enumerated = Array(records.enumerated())
406425 let sorted: [(offset: Int, element: LogRecord)]
@@ -476,12 +495,28 @@ struct LogHeaderView: View {
476495
477496 @State private var draggedColumn: String?
478497 @State private var dropTargetColumn: String?
479− @State private var uniqueValuesCache: [String: [JSONValue]] = [:]
498+ /// `.contextMenu`'s builder runs during body evaluation, so the filter menus
499+ /// compute their values while the view is being rendered. Writing an `@State`
500+ /// dictionary from there is "modifying state during view update" (undefined
501+ /// behavior, and it re-invalidates the header). The cache is therefore a plain
502+ /// reference that isn't observed; `recordsGeneration` — bumped outside body
503+ /// evaluation — is what invalidates it and triggers the re-render.
504+ @State private var uniqueValuesCache = UniqueValuesCache()
505+ @State private var recordsGeneration = 0
506+
507+ private final class UniqueValuesCache {
508+ var generation = -1
509+ var values: [String: [JSONValue]] = [:]
510+ }
480511
481512 private static let maxFilterValues = 20
482513
483514 private func uniqueValues(for column: String) -> [JSONValue] {
484− if let cached = uniqueValuesCache[column] {
515+ if uniqueValuesCache.generation != recordsGeneration {
516+ uniqueValuesCache.generation = recordsGeneration
517+ uniqueValuesCache.values = [:]
518+ }
519+ if let cached = uniqueValuesCache.values[column] {
485520 return cached
486521 }
487522 var seen = Set<JSONValue>()
@@ -493,7 +528,7 @@ struct LogHeaderView: View {
493528 }
494529 }
495530 let sorted = result.sorted()
496− uniqueValuesCache[column] = sorted
531+ uniqueValuesCache.values[column] = sorted
497532 return sorted
498533 }
499534
@@ -636,7 +671,7 @@ struct LogHeaderView: View {
636671 .background(Color(nsColor: .controlBackgroundColor))
637672 .border(Color.secondary.opacity(0.2), width: 0.5)
638673 .onChange(of: records) { _, _ in
639− uniqueValuesCache = [:]
674+ recordsGeneration &+= 1
640675 }
641676 }
642677}
@@ -669,14 +704,26 @@ private struct ColumnDropDelegate: DropDelegate {
669704 }
670705
671706 func performDrop(info: DropInfo) -> Bool {
672− guard let dragged = draggedColumn, dragged != column else {
707+ guard let dragged = draggedColumn, dragged != column,
708+ let provider = info.itemProviders(for: [UTType.plainText]).first else {
673709 draggedColumn = nil
674710 dropTargetColumn = nil
675711 return false
676712 }
677− onMoveColumn?(dragged, column)
678713 draggedColumn = nil
679714 dropTargetColumn = nil
715+ // `draggedColumn` is only cleared by a drop on a header, so a header drag
716+ // that was cancelled (or dropped elsewhere) leaves it set, and any later
717+ // plain-text drag onto a header would move that stale column. Only move
718+ // when the dropped payload really is the column header being dragged.
719+ let target = column
720+ let move = onMoveColumn
721+ _ = provider.loadObject(ofClass: NSString.self) { object, _ in
722+ guard let payload = object as? NSString, payload as String == dragged else { return }
723+ DispatchQueue.main.async {
724+ move?(dragged, target)
725+ }
726+ }
680727 return true
681728 }
682729