Files

142 lines
6.6 KiB
Swift

// SPDX-FileCopyrightText: 2026 Kaede Akino
// SPDX-License-Identifier: GPL-3.0-only
import AgentKit
import Foundation
import TermindApplication
nonisolated public struct ListPanesTool: AgentToolDefinition, AgentToolSchemaBuilding {
public static let presenter = AgentToolDetailPresenter(id: "builtin.list_panes", present: present)
public let panes: AgentPaneAccess
public init(panes: AgentPaneAccess) { self.panes = panes }
public static func present(_ input: AgentToolDetailInput) -> [AgentToolDetail.Item] {
let rows =
input.result.objectValue?["panes"]?.arrayValue?.compactMap { value -> AgentToolDetail.ListRow? in
guard let pane = value.objectValue,
let title = pane["title"]?.stringValue,
let id = pane["pane_id"]?.stringValue
else { return nil }
var badges: [String] = []
if pane["is_active"]?.boolValue == true {
badges.append(AgentPaneToolCopy.localized("Active in window", locale: input.locale))
}
if pane["is_current"]?.boolValue == true {
badges.append(AgentPaneToolCopy.localized("Current in Assistant window", locale: input.locale))
}
return .init(title: title, subtitle: id, badges: badges, symbol: "terminal")
} ?? []
return [
.list(
.init(
rows: rows, emptyMessage: AgentPaneToolCopy.localized("No open terminals", locale: input.locale)))
]
}
public var descriptor: AgentToolDescriptor {
Self.descriptor(
TermindToolName.listPanes,
"List metadata for open terminal panes on this device, including hidden panes. Use read_pane directly when the intended pane_id is already known from session context or a prior result. is_active is per window; is_current identifies the current terminal in this run's window.",
properties: [:], required: [], target: .local, approvalPolicy: .approve,
concurrency: .parallel,
presentation: .init(
symbol: "rectangle.split.2x1", activity: .label("terminals"), output: .json, actionKind: .list)
)
}
public func execute(_ invocation: AgentToolInvocation, context: AgentToolExecutionContext) async throws
-> AgentToolResult
{
let rows = await panes.list().map { pane in
AgentJSONValue.object([
"pane_id": .string(pane.id.uuidString),
"title": .string(pane.title),
"host_id": pane.hostID.map { .string($0.uuidString) } ?? .null,
"workspace": .string(pane.workspace),
"window": .string(pane.window),
"is_active": .bool(pane.isActive),
"is_current": .bool(pane.isCurrent),
])
}
return Self.result(invocation, .object(["panes": .array(rows)]))
}
}
nonisolated public struct ReadPaneTool: AgentToolDefinition, AgentToolSchemaBuilding {
public static let presenter = AgentToolDetailPresenter(
id: "builtin.read_pane", layout: .init(truncateStrategy: .none), present: present)
public let panes: AgentPaneAccess
public var truncateStrategy: AgentTruncateStrategy { .none }
public init(panes: AgentPaneAccess) { self.panes = panes }
public static func present(_ input: AgentToolDetailInput) -> [AgentToolDetail.Item] {
let object = input.result.objectValue ?? [:]
var items: [AgentToolDetail.Item] = []
if let title = object["title"]?.stringValue {
items.append(
.field(
.init(
label: AgentPaneToolCopy.localized("Terminal name", locale: input.locale), value: title)))
}
if let id = object["pane_id"]?.stringValue ?? input.arguments["pane_id"]?.stringValue {
items.append(
.field(
.init(
label: AgentPaneToolCopy.localized("Pane ID", locale: input.locale),
value: id, isMonospaced: true)))
}
return items
}
public var descriptor: AgentToolDescriptor {
Self.descriptor(
TermindToolName.readPane,
"Read the visible viewport of an open terminal pane on this device while preserving focus, scroll position, selection, and clipboard. pane_id is required: use the terminal ID from session context or a prior tool result. Call read_pane directly when the intended ID is known; use list_panes to discover an unknown or unavailable target.",
properties: ["pane_id": Self.string(max: 64)], required: ["pane_id"],
target: .local, approvalPolicy: .approve, concurrency: .parallel,
presentation: .init(symbol: "terminal", activity: .label("terminal"), output: .json, actionKind: .read)
)
}
public func execute(_ invocation: AgentToolInvocation, context: AgentToolExecutionContext) async throws
-> AgentToolResult
{
let arguments = try Arguments(invocation)
guard let id = UUID(uuidString: try arguments.string("pane_id")) else {
throw AgentToolError.invalidArguments(
"pane_id is not a valid UUID. Use the intended pane ID from session context or list_panes.")
}
let read = await panes.read(id)
switch read {
case .closed:
throw AgentToolError.invalidArguments("Pane is not open. Call list_panes for a current pane ID.")
case .notConnected:
throw AgentToolError.invalidArguments("The pane is not connected. Call list_panes for a readable pane ID.")
case .noSurface:
throw AgentToolError.invalidArguments("The pane's terminal surface is not established.")
case .text(let pane, let text):
return Self.result(
invocation,
.object([
"pane_id": .string(pane.id.uuidString),
"title": .string(pane.title),
"host_id": pane.hostID.map { .string($0.uuidString) } ?? .null,
"text": .string(text),
"truncated": .bool(false),
]))
}
}
}
nonisolated private enum AgentPaneToolCopy {
// These keys are owned by shared AppStrings and the app's manually managed catalog.
// This UI-neutral presenter cannot import Presentation; the app projection
// uses AppStrings directly and this fallback resolves the same catalog keys.
static func localized(_ key: String.LocalizationValue, locale: Locale) -> String {
AppLocalization.string(key, locale: locale)
}
}