Files
hapi/ios/Hapi/Features/Chat/Blocks/ToolSummaryPresentation.swift
T

79 lines
3.9 KiB
Swift

import Foundation
import HapiProtocol
import HapiUI
import SwiftUI
extension HapiTypography {
// Use the same resolved metrics as markdown, not the UIKit hosting cell's
// system font defaults. Baselines remain subheadline (15) / footnote (13).
var toolTitleFont: Font { .system(size: 15 * bodyScale, weight: boldText ? .semibold : .medium) }
var toolSubtitleFont: Font { .system(size: 13 * codeScale, weight: boldText ? .semibold : .regular) }
var toolIconWidth: CGFloat { 18 * codeScale }
var usesStackedToolLayout: Bool { bodySize > 24 }
}
/// Conversation-only labels. Keep the inspector's original tool/input presentation
/// intact: a useful activity summary is not a replacement for the recorded call.
func toolSummaryPresentation(_ tool: ChatToolCall, basePath: String?) -> ToolCardPresentation {
var canonical = tool
canonical.name = toolPresentationName(tool.name)
var presentation = toolCardPresentation(canonical, basePath: basePath)
let input = tool.input
func file(_ action: String, keys: [String], icon: String? = nil) -> ToolCardPresentation {
let target = chatInputString(input, keys).map(summaryBasename)
return ToolCardPresentation(
icon: icon ?? presentation.icon,
title: target.map { "\(action) · \($0)" } ?? action,
subtitle: nil
)
}
switch canonical.name {
case "Read", "NotebookRead":
return file(String(localized: "Read file"), keys: ["file_path", "path", "file", "notebook_path"])
case "Edit", "MultiEdit", "NotebookEdit":
return file(String(localized: "Edit file"), keys: ["file_path", "path", "notebook_path"])
case "Write":
return file(String(localized: "Write file"), keys: ["file_path", "path"])
case "LS":
return file(String(localized: "List files"), keys: ["path"])
case "CodexBash":
if let parsed = input?[chatKey: "parsed_cmd"]?.chatArray, parsed.count == 1,
parsed[0][chatKey: "type"]?.chatString == "read",
let path = parsed[0][chatKey: "name"]?.chatString {
return ToolCardPresentation(icon: "eye", title: "\(String(localized: "Read file")) · \(summaryBasename(path))", subtitle: nil)
}
case "view_image":
return file(String(localized: "View image"), keys: ["path", "file_path", "image_path"], icon: "photo")
case "mcp__hapi__display_image", "hapi__display_image", "hapi_display_image":
return file(String(localized: "Show image"), keys: ["path"], icon: "photo")
case "mcp__hapi__display_video", "hapi__display_video", "hapi_display_video":
return file(String(localized: "Show video"), keys: ["path"], icon: "film")
case "mcp__hapi__display_media", "hapi__display_media", "hapi_display_media":
return file(String(localized: "Share file"), keys: ["path"], icon: "doc")
case "exec":
return ToolCardPresentation(icon: "curlybraces", title: tool.description ?? String(localized: "Run script"), subtitle: nil)
case "wait":
return ToolCardPresentation(icon: "hourglass", title: String(localized: "Wait for result"), subtitle: nil)
case "write_stdin":
return ToolCardPresentation(icon: "terminal", title: String(localized: "Continue command"), subtitle: nil)
default:
break
}
if let subtitle = presentation.subtitle {
// Don't waste the preview on indentation, line breaks, or a long directory.
if let path = chatInputString(input, ["file_path", "path", "filePath", "file"]), subtitle == chatTruncate(path, 80) {
presentation.subtitle = summaryBasename(path)
} else {
presentation.subtitle = subtitle.prefix(240).split(whereSeparator: \.isWhitespace).joined(separator: " ")
}
if presentation.subtitle == presentation.title { presentation.subtitle = nil }
}
return presentation
}
private func summaryBasename(_ path: String) -> String {
path.split(separator: "/").last.map(String.init) ?? path
}