114 lines
4.7 KiB
Swift
114 lines
4.7 KiB
Swift
// SPDX-FileCopyrightText: 2026 Kaede Akino
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
import AgentKit
|
|
import Foundation
|
|
|
|
/// A whole-card budget, not one budget per block. Full values remain untouched for expansion.
|
|
nonisolated public struct AgentToolDetailPreview: Equatable, Sendable {
|
|
public static let byteLimit = AgentTruncateLimits.maximumBytes
|
|
public static let rowLimit = 5
|
|
public let items: [AgentToolDetail.Item]
|
|
public let fullItems: [AgentToolDetail.Item]
|
|
public var isShortened: Bool { items != fullItems }
|
|
|
|
public init(_ detail: AgentToolDetail) {
|
|
fullItems = detail.items
|
|
guard detail.layout.truncateStrategy != .none else {
|
|
items = detail.items
|
|
return
|
|
}
|
|
var budget = Budget(strategy: detail.layout.truncateStrategy)
|
|
items = budget.project(detail.items)
|
|
}
|
|
|
|
public var fullText: String { Self.text(fullItems) }
|
|
|
|
private static func text(_ items: [AgentToolDetail.Item]) -> String {
|
|
items.map { item in
|
|
switch item {
|
|
case .field(let field): return field.value.isEmpty ? field.label : field.label + ": " + field.value
|
|
case .message(let value, _): return value
|
|
case .text(let block): return [block.title, block.text].compactMap { $0 }.joined(separator: "\n")
|
|
case .list(let list):
|
|
return
|
|
([list.title].compactMap { $0 } + list.rows.map(rowText)
|
|
+ (list.rows.isEmpty ? [list.emptyMessage].compactMap { $0 } : [])).joined(separator: "\n")
|
|
case .groupedList(let list):
|
|
return
|
|
([list.summary]
|
|
+ list.sections.map {
|
|
([$0.title] + $0.rows.map(rowText)).joined(separator: "\n")
|
|
}).joined(separator: "\n")
|
|
}
|
|
}.joined(separator: "\n\n")
|
|
}
|
|
|
|
private static func rowText(_ row: AgentToolDetail.ListRow) -> String {
|
|
([row.title, row.subtitle, row.detail].compactMap { $0 } + row.badges).joined(separator: "\n")
|
|
}
|
|
|
|
private struct Budget {
|
|
var rows = AgentToolDetailPreview.rowLimit
|
|
var bytes = AgentToolDetailPreview.byteLimit
|
|
var strategy: AgentTruncateStrategy
|
|
|
|
mutating func project(_ items: [AgentToolDetail.Item]) -> [AgentToolDetail.Item] {
|
|
var result: [AgentToolDetail.Item] = []
|
|
for item in items {
|
|
switch item {
|
|
case .field, .message:
|
|
// Command metadata and notices are never part of the body budget.
|
|
result.append(item)
|
|
case .text(var block):
|
|
if !block.isCollapsible {
|
|
result.append(item)
|
|
continue
|
|
}
|
|
let selected = AgentTruncateResult.apply(
|
|
block.text, strategy: strategy,
|
|
limits: .init(bytes: bytes, lines: max(0, rows)))
|
|
rows -= selected.returnedLines
|
|
bytes -= selected.content.utf8.count
|
|
block.text = selected.content
|
|
if !block.text.isEmpty { result.append(.text(block)) }
|
|
case .list(var list):
|
|
list.rows = projectRows(list.rows)
|
|
result.append(.list(list))
|
|
case .groupedList(var list):
|
|
list.sections = list.sections.compactMap { section in
|
|
guard rows > 0 else { return nil }
|
|
var section = section
|
|
section.rows = projectRows(section.rows)
|
|
return section
|
|
}
|
|
result.append(.groupedList(list))
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
mutating func projectRows(_ values: [AgentToolDetail.ListRow]) -> [AgentToolDetail.ListRow] {
|
|
let count = min(values.count, max(0, rows))
|
|
let indices: [Int]
|
|
switch strategy {
|
|
case .head, .none:
|
|
indices = Array(values.indices.prefix(count))
|
|
case .tail:
|
|
indices = Array(values.indices.reversed().prefix(count))
|
|
case .middle:
|
|
indices = (0..<count).map { $0.isMultiple(of: 2) ? $0 / 2 : values.count - 1 - $0 / 2 }
|
|
}
|
|
var selected: [Int] = []
|
|
for index in indices {
|
|
let size = AgentToolDetailPreview.rowText(values[index]).utf8.count
|
|
guard size <= bytes else { break }
|
|
selected.append(index)
|
|
bytes -= size
|
|
}
|
|
rows -= selected.count
|
|
return selected.sorted().map { values[$0] }
|
|
}
|
|
}
|
|
}
|