Drop the modules/threads tabs; show both as collapsible sidebar groups mirroring the hooks UX: a handful of top picks plus a browse popover for the rest. Default threads to collapsed. Revert the eager re-analysis of persisted modules on attach; keep disassembly analysis lazy and add a per-module Analyze action and status indicator instead. Share one sidebar-row and one browser-popover component across hooks, modules, and threads in both the SwiftUI and GTK frontends.
93 lines
3.1 KiB
Swift
93 lines
3.1 KiB
Swift
import LumaCore
|
|
import SwiftUI
|
|
|
|
struct ThreadSidebarChildren: View {
|
|
let sessionID: UUID
|
|
let engine: Engine
|
|
@Binding var selection: SidebarItemID?
|
|
|
|
var body: some View {
|
|
let threads = engine.session(id: sessionID)?.lastKnownThreads ?? []
|
|
let highlights = threads.sidebarHighlights(selectedID: selectedThreadID)
|
|
ForEach(highlights) { thread in
|
|
ThreadSidebarRow(sessionID: sessionID, thread: thread, engine: engine, selection: $selection)
|
|
.tag(SidebarItemID.thread(sessionID, thread.id))
|
|
}
|
|
if threads.count > highlights.count {
|
|
SidebarBrowseAllRow(count: threads.count) { dismiss in
|
|
ThreadBrowserPopover(
|
|
sessionID: sessionID,
|
|
threads: threads,
|
|
selection: $selection,
|
|
onDismiss: dismiss
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
private var selectedThreadID: ProcessThread.ID? {
|
|
if case .thread(let sid, let tid) = selection, sid == sessionID { return tid }
|
|
return engine.lastSelectedThreadID(for: sessionID)
|
|
}
|
|
}
|
|
|
|
private struct ThreadSidebarRow: View {
|
|
let sessionID: UUID
|
|
let thread: ProcessThread
|
|
let engine: Engine
|
|
@Binding var selection: SidebarItemID?
|
|
|
|
var body: some View {
|
|
SidebarFeatureRowLabel(
|
|
icon: { Image(systemName: "cpu").font(.system(size: 11)) },
|
|
title: thread.name ?? "tid \(thread.id)",
|
|
help: "tid \(thread.id)"
|
|
)
|
|
.contextMenu {
|
|
let actions = engine.threadActions(sessionID: sessionID, thread: thread)
|
|
ForEach(actions) { action in
|
|
Button(role: action.role == .destructive ? .destructive : nil) {
|
|
Task { @MainActor in
|
|
if let target = await action.perform() {
|
|
selection = SidebarItemID(navigationTarget: target)
|
|
}
|
|
}
|
|
} label: {
|
|
if let icon = action.systemImage {
|
|
Label(action.title, systemImage: icon)
|
|
} else {
|
|
Text(action.title)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct ThreadBrowserPopover: View {
|
|
let sessionID: UUID
|
|
let threads: [ProcessThread]
|
|
@Binding var selection: SidebarItemID?
|
|
let onDismiss: () -> Void
|
|
|
|
var body: some View {
|
|
SidebarBrowserPopover(
|
|
placeholder: "Filter threads",
|
|
emptyMessage: "No matching threads",
|
|
items: threads,
|
|
groupName: { $0.name == nil ? "Unnamed" : "Named" },
|
|
title: { $0.name ?? "tid \($0.id)" },
|
|
help: { "tid \($0.id)" },
|
|
isDimmed: { _ in false },
|
|
matches: { thread, query in
|
|
(thread.name?.localizedCaseInsensitiveContains(query) ?? false)
|
|
|| String(thread.id).localizedCaseInsensitiveContains(query)
|
|
},
|
|
onChoose: { thread in
|
|
selection = .thread(sessionID, thread.id)
|
|
onDismiss()
|
|
}
|
|
)
|
|
}
|
|
}
|