diff --git a/Frameworks/Resources/Localizable.xcstrings b/Frameworks/Resources/Localizable.xcstrings index 190cac1d..b38bb4d7 100644 --- a/Frameworks/Resources/Localizable.xcstrings +++ b/Frameworks/Resources/Localizable.xcstrings @@ -2935,6 +2935,23 @@ } } }, + "Approval action target: %@ %@" : { + "extractionState" : "stale", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "在%2$@上%1$@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "在%2$@上%1$@" + } + } + } + }, "Approval required" : { "localizations" : { "zh-Hans" : { @@ -2951,18 +2968,36 @@ } } }, - "Arguments" : { + "Approval tool action target: %@ %@ %@" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "参数" + "value" : "在%3$@上%1$@%2$@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "參數" + "value" : "在%3$@上%1$@%2$@" + } + } + } + }, + "Approval tool action: %@ %@" : { + "extractionState" : "stale", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@%2$@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@%2$@" } } } @@ -3015,6 +3050,23 @@ } } }, + "Arguments" : { + "extractionState" : "stale", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "参数" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "參數" + } + } + } + }, "Armed" : { "extractionState" : "manual", "localizations" : { @@ -10793,6 +10845,7 @@ } }, "Host: %@" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -19038,6 +19091,7 @@ } }, "Safety details" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -26868,4 +26922,4 @@ } }, "version" : "1.2" -} +} \ No newline at end of file diff --git a/Frameworks/TermindPresentation/AgentToolPresentation.swift b/Frameworks/TermindPresentation/AgentToolPresentation.swift index 2a0d3d65..51355cf0 100644 --- a/Frameworks/TermindPresentation/AgentToolPresentation.swift +++ b/Frameworks/TermindPresentation/AgentToolPresentation.swift @@ -42,7 +42,7 @@ nonisolated public enum AgentToolPresentation { ) return AgentApprovalPresentation( symbol: symbol(for: card), - title: summary(for: card, hostName: hostName, locale: locale), + title: approvalSummary(for: card, hostName: hostName, locale: locale), detail: AgentToolDetail( items: approvalArgumentItems( request.invocation.call.arguments, presenter: presenter, @@ -51,6 +51,65 @@ nonisolated public enum AgentToolPresentation { ) } + /// Approval chrome already communicates that the call is waiting. Keep its + /// action title short and move argument values into the reviewable detail + /// below, where long commands and paths can scroll without hiding actions. + private static func approvalSummary( + for card: AgentToolCard, + hostName: String?, + locale: Locale + ) -> String { + let activity = approvalActivity(for: card, locale: locale) + guard let kind = actionKind(for: card) else { + let action = localized("Run \(activity)", locale: locale) + guard let hostName, !hostName.isEmpty else { return action } + if isChinese(locale) { + return localized("Approval action target: \(action) \(hostName)", locale: locale) + } + return "\(action) on \(hostName)" + } + + let action = localizedActionVerb(kind, locale: locale) + if isChinese(locale) { + guard let hostName, !hostName.isEmpty else { + return localized("Approval tool action: \(action) \(activity)", locale: locale) + } + return localized( + "Approval tool action target: \(action) \(activity) \(hostName)", + locale: locale + ) + } + + let rendered = + uppercaseFirst(actionName(kind, form: .infinitive, locale: locale)) + + (activity.isEmpty ? "" : " \(activity)") + guard let hostName, !hostName.isEmpty else { return rendered } + return "\(rendered) on \(hostName)" + } + + private static func approvalActivity( + for card: AgentToolCard, + locale: Locale + ) -> String { + guard let presentation = card.descriptor.presentation else { + return humanized(card.descriptor.qualifiedName) + } + switch presentation.activity { + case .label(let label): + return legacyActivity( + label, descriptorName: card.descriptor.name, locale: locale + ) + case .argument(_, let fallback): + return legacyActivity( + fallback, descriptorName: card.descriptor.name, locale: locale + ) + case .arrayCount, .localizedArrayCount, .semanticArrayCount: + return operation(for: card, locale: locale).text + case .semanticLabel(let kind), .semanticArgument(_, let kind): + return activityName(kind, locale: locale) + } + } + /// While work is live, use the icon of the tool(s) still in flight. After /// completion, a homogeneous batch keeps its tool icon; mixed batches use /// the generic tools symbol. @@ -1104,6 +1163,11 @@ nonisolated public enum AgentToolPresentation { return first.lowercased() + String(text.dropFirst()) } + private static func uppercaseFirst(_ text: String) -> String { + guard let first = text.first else { return text } + return first.uppercased() + String(text.dropFirst()) + } + public static func tint(for state: AgentToolCard.State) -> BatchTint { switch state { case .denied, .failed: .failure diff --git a/TFI/Features/AI/MobileAssistantInteraction.swift b/TFI/Features/AI/MobileAssistantInteraction.swift index ee6d97d1..8b8100b6 100644 --- a/TFI/Features/AI/MobileAssistantInteraction.swift +++ b/TFI/Features/AI/MobileAssistantInteraction.swift @@ -72,7 +72,7 @@ struct MobileAssistantInteraction: View { .accessibilityHidden(true) Text(presentation.title) .font(.callout.weight(.medium)) - .fixedSize(horizontal: false, vertical: true) + .lineLimit(3) } ViewThatFits(in: .vertical) { @@ -99,6 +99,8 @@ struct MobileAssistantInteraction: View { .frame(minHeight: 44) .accessibilityIdentifier("AssistantApproval.AllowOnce") } + .fixedSize(horizontal: false, vertical: true) + .layoutPriority(2) } .frame(maxWidth: .infinity, alignment: .topLeading) .accessibilityElement(children: .contain) diff --git a/TFM/Features/AI/ConversationTranscriptView.swift b/TFM/Features/AI/ConversationTranscriptView.swift index ae6fa0b3..451db818 100644 --- a/TFM/Features/AI/ConversationTranscriptView.swift +++ b/TFM/Features/AI/ConversationTranscriptView.swift @@ -550,7 +550,7 @@ private struct AgentApprovalCard: View { .accessibilityHidden(true) Text(presentation.title) .font(.callout.weight(.medium)) - .fixedSize(horizontal: false, vertical: true) + .lineLimit(3) } ViewThatFits(in: .vertical) { @@ -575,6 +575,8 @@ private struct AgentApprovalCard: View { .buttonStyle(.borderedProminent) .accessibilityIdentifier("AssistantApproval.AllowOnce") } + .fixedSize(horizontal: false, vertical: true) + .layoutPriority(2) } .accessibilityElement(children: .contain) .accessibilityIdentifier("AssistantApproval.Card") diff --git a/TFMTests/AgentToolPresentationTests.swift b/TFMTests/AgentToolPresentationTests.swift index 300706e7..2f40ed49 100644 --- a/TFMTests/AgentToolPresentationTests.swift +++ b/TFMTests/AgentToolPresentationTests.swift @@ -55,8 +55,17 @@ struct AgentToolPresentationTests { ) #expect(presentation.symbol == "terminal") - #expect(presentation.title.contains("Production")) + #expect(presentation.title == "Run the SSH command on Production") + #expect(!presentation.title.contains(command)) #expect(!presentation.title.contains(hostID.uuidString)) + #expect( + AgentToolPresentation.approval( + for: request, + presenter: SSHExecTool.presenter, + hostName: "Production", + locale: Locale(identifier: "zh-Hans") + ).title == "在Production上运行SSH命令" + ) #expect( presentation.detail.items == [ .text(.init(title: "Command", text: command, style: .monospaced)), @@ -88,6 +97,8 @@ struct AgentToolPresentationTests { let presentation = AgentToolPresentation.approval( for: request, locale: Locale(identifier: "en") ) + #expect(presentation.title == "Run remote tool") + #expect(!presentation.title.contains("hunter2")) #expect( presentation.detail.items == [ .field(.init(label: "Password", value: "hunter2"))