// SPDX-FileCopyrightText: 2026 Kaede Akino // SPDX-License-Identifier: GPL-3.0-only import AgentKit import Foundation import TermindApplication /// What Termind's own agent tools are built from. /// /// The runtime's `AgentBuiltInToolConfiguration` covers the tools it ships; /// this covers ours, which are all about hosts — listing them, running commands /// on them, moving files to and from them. Tool types stay narrowly injected: /// the environment is unpacked here and each definition receives only what it /// uses. nonisolated public struct TermindAgentToolEnvironment: Sendable { public var metrics: any MetricsSnapshotReading public var hosts: AgentHostResolver public var connections: AgentConnectionPool public var authentication: AgentSSHAuthenticationRegistry public var scope: UUID public var background: AgentBackgroundCommands /// Shared with the runtime's scratch group. `scratch_pull` and /// `scratch_push` are ours rather than the runtime's precisely because they /// are the two ends that touch a host, and they must stage into the same /// directory `scratch_replace` reads. public var workspace: AgentScratchWorkspace? public var panes: AgentPaneAccess? public init( hosts: AgentHostResolver, metrics: any MetricsSnapshotReading, connections: AgentConnectionPool, authentication suppliedAuthentication: AgentSSHAuthenticationRegistry? = nil, background: AgentBackgroundCommands = AgentBackgroundCommands(), scope: UUID = UUID(), workspace: AgentScratchWorkspace? = nil, panes: AgentPaneAccess? = nil ) { self.metrics = metrics self.hosts = hosts self.connections = connections authentication = suppliedAuthentication ?? AgentSSHAuthenticationRegistry(resolver: hosts) self.background = background self.scope = scope self.workspace = workspace self.panes = panes } } /// Termind's own agent tool catalog, registered alongside the runtime's. /// /// It exists for the same reason the runtime's does: one registration point per /// type, supplying both the executable definition and the presenter a persisted /// card is redrawn from years later. nonisolated public enum TermindAgentTools { public typealias Registration = AgentToolTypeRegistration public static func tools(in environment: TermindAgentToolEnvironment) -> [AnyAgentTool] { definitions.compactMap { $0.tool(in: environment) } } /// Every presenter that can draw a card in this app, ours after the /// runtime's. Built once: the map is immutable and the lookup happens on /// every historical card the transcript draws. public static let presenterRegistry = AgentToolPresenterRegistry( AgentToolCatalog.builtInPresenters + definitions.map(\.presenter) ) public static func presenter(id: String) -> AgentToolDetailPresenter? { presenterRegistry.presenter(id: id) } public static func presenter(for descriptor: AgentToolDescriptor) -> AgentToolDetailPresenter? { guard descriptor.target != .mcp, let id = descriptor.presentation?.presenterID else { return nil } return presenter(id: id) } private static let definitions: [Registration] = [ .init(GetMetricsTool.self) { GetMetricsTool(hosts: $0.hosts, metrics: $0.metrics) }, .init(ListHostsTool.self) { ListHostsTool(hosts: $0.hosts) }, .init(ListPanesTool.self) { $0.panes.map(ListPanesTool.init) }, .init(ReadPaneTool.self) { $0.panes.map(ReadPaneTool.init) }, .init(SSHExecRunTool.self, availableIn: [.acting]) { SSHExecRunTool( authentication: $0.authentication, pool: $0.connections, background: $0.background, scope: $0.scope ) }, .init(PlanningSSHExecRunTool.self, availableIn: [.planning]) { PlanningSSHExecRunTool( authentication: $0.authentication, pool: $0.connections, background: $0.background, scope: $0.scope ) }, .init(SSHExecOutputTool.self) { SSHExecOutputTool(background: $0.background, scope: $0.scope) }, .init(SSHExecStdinTool.self, availableIn: [.acting]) { SSHExecStdinTool(background: $0.background, scope: $0.scope) }, .init(PlanningSSHExecStdinTool.self, availableIn: [.planning]) { PlanningSSHExecStdinTool(background: $0.background, scope: $0.scope) }, .init(SSHExecCancelTool.self) { SSHExecCancelTool(background: $0.background, scope: $0.scope) }, .init(SSHExecListTool.self) { SSHExecListTool(background: $0.background, scope: $0.scope) }, .init(TerminalSuggestTool.self) { _ in TerminalSuggestTool() }, .init(SFTPListTool.self) { SFTPListTool(authentication: $0.authentication, connections: $0.connections) }, .init(SFTPStatTool.self) { SFTPStatTool(authentication: $0.authentication, connections: $0.connections) }, .init(SFTPReadTool.self) { SFTPReadTool(authentication: $0.authentication, connections: $0.connections) }, .init(SFTPWriteTool.self, availableIn: [.acting]) { SFTPWriteTool(authentication: $0.authentication, connections: $0.connections) }, .init(SFTPMkdirTool.self, availableIn: [.acting]) { SFTPMkdirTool(authentication: $0.authentication, connections: $0.connections) }, .init(SFTPMoveTool.self, availableIn: [.acting]) { SFTPMoveTool(authentication: $0.authentication, connections: $0.connections) }, .init(SFTPCopyTool.self, availableIn: [.acting]) { SFTPCopyTool(hosts: $0.hosts, authentication: $0.authentication, connections: $0.connections) }, .init(SFTPChmodTool.self, availableIn: [.acting]) { SFTPChmodTool(authentication: $0.authentication, connections: $0.connections) }, .init(SFTPDeleteTool.self, availableIn: [.acting]) { SFTPDeleteTool(authentication: $0.authentication, connections: $0.connections) }, .init(ScratchPullTool.self) { environment in environment.workspace.map { ScratchPullTool( workspace: $0, pool: environment.connections, authentication: environment.authentication ) } }, .init(ScratchPushTool.self, availableIn: [.acting]) { environment in environment.workspace.map { ScratchPushTool( workspace: $0, pool: environment.connections, authentication: environment.authentication ) } }, ] }