Files
hapi/ios/Hapi/Features/Chat/ToolGroupBrowser.swift
T

87 lines
3.7 KiB
Swift

import HapiProtocol
import HapiUI
import SwiftUI
/// The list stays mounted behind its detail destination. Neither live data nor
/// returning from a detail is a scroll command; only opening and Latest are.
struct ToolGroupBrowser: View {
let group: ToolGroupBlock
let basePath: String?
let isStale: Bool
let isReconnecting: Bool
let selectTool: (String) -> Void
let close: () -> Void
@State private var positionedInitially = false
@Environment(\.hapiTheme) private var theme
var body: some View {
ScrollViewReader { proxy in
List {
ForEach(group.tools, id: \.id) { block in
ToolSummaryRow(presentation: toolSummaryPresentation(block.tool, basePath: basePath),
state: block.tool.state) {
selectTool(block.id)
}
.accessibilityHint(opensToolProcess(block)
? String(localized: "View agent process") : String(localized: "View tool details"))
.accessibilityIdentifier("tool-browser-row-\(block.id)")
.hapiReadingColumn()
.listRowInsets(EdgeInsets())
.listRowBackground(theme.surface)
.listRowSeparatorTint(theme.divider)
.id(block.id)
}
}
.listStyle(.plain)
.scrollContentBackground(.hidden)
.background(theme.background)
.accessibilityIdentifier("tool-browser")
.task {
guard !positionedInitially, let id = group.tools.last?.id else { return }
positionedInitially = true
proxy.scrollTo(id, anchor: .bottom)
}
.safeAreaInset(edge: .top, spacing: 0) {
VStack(alignment: .leading, spacing: 6) {
Text(ToolGroupPresentation(group).categorySummary)
.lineLimit(2)
.foregroundStyle(theme.textSecondary)
if isStale {
Label("Showing the last available group. Live group updates are unavailable.", systemImage: "clock.arrow.circlepath")
} else if isReconnecting {
Label("Live updates interrupted — reconnecting…", systemImage: "wifi.slash")
}
if group.needsOlderHistory {
Text("Earlier calls aren't loaded. Close this view and load older messages in the conversation.")
}
}
.font(.footnote)
.hapiReadingColumn()
.padding(.vertical, 8)
.background(.bar)
}
.safeAreaInset(edge: .bottom, spacing: 0) {
Button {
if let id = group.tools.last?.id { proxy.scrollTo(id, anchor: .bottom) }
} label: {
Label("Latest tool", systemImage: "arrow.down")
.frame(maxWidth: .infinity, minHeight: 44)
}
.disabled(group.tools.isEmpty)
.accessibilityIdentifier("tool-browser-latest")
.hapiReadingColumn()
.padding(.vertical, 4)
.background(.bar)
}
}
.navigationTitle(ToolGroupPresentation(group).title)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Close", systemImage: "xmark", action: close)
.accessibilityIdentifier("tool-inspector-close")
}
}
}
}