Nucleic: Collapseable session transcript blocks
This commit is contained in:
@@ -20,11 +20,7 @@ struct ToolCallCard: View {
|
|||||||
// Folded lock-lifecycle lines read as part of the edit card (always visible, not gated
|
// Folded lock-lifecycle lines read as part of the edit card (always visible, not gated
|
||||||
// by expand), so the lock brackets its edit rather than sorting after it — as on the Mac.
|
// by expand), so the lock brackets its edit rather than sorting after it — as on the Mac.
|
||||||
if !group.lockLines.isEmpty {
|
if !group.lockLines.isEmpty {
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
LockLinesSection(locks: group.lockLines)
|
||||||
ForEach(Array(group.lockLines.enumerated()), id: \.offset) { _, lock in
|
|
||||||
LockLineRow(lock: lock)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if expanded { details }
|
if expanded { details }
|
||||||
}
|
}
|
||||||
@@ -570,6 +566,99 @@ struct LockLineRow: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The whole lock trail folded onto one tool card — the phone echo of the Mac's
|
||||||
|
/// `LockLinesSection`. A short trail (an ordinary edit's waiting → acquired → released) stays
|
||||||
|
/// literal; a long one — a supervising turn holding the lock for every file its workers touch —
|
||||||
|
/// collapses to one summary line that expands into a `LockDigest`: one entry per path at its
|
||||||
|
/// latest state, grouped under its directory so a shared prefix is written once.
|
||||||
|
struct LockLinesSection: View {
|
||||||
|
let locks: [NoteLock]
|
||||||
|
/// Trails at or below this many events render as plain lines (the common edit card).
|
||||||
|
static let inlineLimit = 4
|
||||||
|
@State private var expanded = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if locks.count <= Self.inlineLimit {
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
ForEach(Array(locks.enumerated()), id: \.offset) { _, lock in
|
||||||
|
LockLineRow(lock: lock)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let digest = LockDigest(locks)
|
||||||
|
VStack(alignment: .leading, spacing: 5) {
|
||||||
|
Button {
|
||||||
|
withAnimation(.easeInOut(duration: 0.15)) { expanded.toggle() }
|
||||||
|
} label: { summary(digest) }
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
if expanded { LockDigestBody(digest: digest) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func summary(_ digest: LockDigest) -> some View {
|
||||||
|
HStack(alignment: .firstTextBaseline, spacing: 6) {
|
||||||
|
Image(systemName: LockLineStyle.icon(digest.dominantState))
|
||||||
|
.font(.caption2).foregroundStyle(.tertiary).frame(width: 15)
|
||||||
|
Text(digest.headline)
|
||||||
|
.font(.caption).foregroundStyle(.secondary)
|
||||||
|
Image(systemName: expanded ? "chevron.down" : "chevron.right")
|
||||||
|
.font(.system(size: 8, weight: .semibold)).foregroundStyle(.tertiary)
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
}
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The expanded lock digest: a block per state, and within it one line per directory carrying
|
||||||
|
/// that directory's basenames. A directory holding a single file collapses back to its full path.
|
||||||
|
private struct LockDigestBody: View {
|
||||||
|
let digest: LockDigest
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
let sections = digest.sections
|
||||||
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
|
ForEach(sections, id: \.state) { section in
|
||||||
|
VStack(alignment: .leading, spacing: 3) {
|
||||||
|
// With one state the summary line above already names it; only a mixed
|
||||||
|
// trail needs each block labelled.
|
||||||
|
if sections.count > 1 {
|
||||||
|
HStack(spacing: 5) {
|
||||||
|
Image(systemName: LockLineStyle.icon(section.state))
|
||||||
|
.font(.system(size: 9)).foregroundStyle(.tertiary)
|
||||||
|
Text("\(LockDigest.sectionTitle(section.state)) · \(section.count)")
|
||||||
|
.font(.caption2.weight(.medium)).foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ForEach(section.directories, id: \.directory) { directory in
|
||||||
|
group(directory)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.leading, 21)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func group(_ directory: LockDigest.Directory) -> some View {
|
||||||
|
if let path = directory.singlePath {
|
||||||
|
Text(path)
|
||||||
|
.font(.caption2.monospaced()).foregroundStyle(.secondary)
|
||||||
|
.lineLimit(1).truncationMode(.middle)
|
||||||
|
} else {
|
||||||
|
VStack(alignment: .leading, spacing: 1) {
|
||||||
|
Text(directory.directory.isEmpty ? "./" : "\(directory.directory)/")
|
||||||
|
.font(.caption2.monospaced()).foregroundStyle(.tertiary)
|
||||||
|
.lineLimit(1).truncationMode(.middle)
|
||||||
|
Text(directory.files.joined(separator: ", "))
|
||||||
|
.font(.caption2).foregroundStyle(.secondary)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
.padding(.leading, 10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// SF Symbols for the common tools + file-change kinds, so a glance reads the action.
|
/// SF Symbols for the common tools + file-change kinds, so a glance reads the action.
|
||||||
enum ToolGlyph {
|
enum ToolGlyph {
|
||||||
static func icon(_ name: String) -> String {
|
static func icon(_ name: String) -> String {
|
||||||
|
|||||||
Reference in New Issue
Block a user