Improve Simplified Chinese tool presentation and localization

This commit is contained in:
Kaede Akino committed 2026-08-27 14:13:54 +08:00
1 parent af0dd561e2
commit c8229ff645
22 files changed
+1435 -261

No files matched your search

+63
View File
@@ -0,0 +1,63 @@
#!/bin/bash
set -euo pipefail
script_dir=$(cd "$(dirname "$0")" && pwd)
catalog=${1:-"$script_dir/../Termind/Resources/Localizable.xcstrings"}
if ! jq empty "$catalog"; then
echo "Invalid string catalog: $catalog" >&2
exit 1
fi
missing=$(jq -r '
.strings | to_entries[]
| select(.value.localizations."zh-Hans" == null)
| .key
' "$catalog")
if [[ -n "$missing" ]]; then
echo "Missing zh-Hans localizations:" >&2
echo "$missing" >&2
exit 1
fi
unfinished=$(jq -r '
.strings | to_entries[] as $entry
| [
$entry.value.localizations."zh-Hans"
| .. | objects | .stringUnit?
| select(. != null and .state != "translated")
]
| select(length > 0)
| $entry.key
' "$catalog")
if [[ -n "$unfinished" ]]; then
echo "Unfinished zh-Hans localizations:" >&2
echo "$unfinished" >&2
exit 1
fi
violations=$(jq -r '
.strings | to_entries[] as $entry
| [
($entry.value.localizations."zh-Hans" // {})
| .. | objects | .stringUnit?.value? // empty
]
| .[]
| select(
test("\\p{Han}")
and (
test("\\p{Han}[ \\t]+[A-Za-z0-9%@`]|[A-Za-z0-9%@`][ \\t]+\\p{Han}")
or test("[ \\t]+[—·→:,。!?;、/:]|[—·→:,。!?;、/:][ \\t]+")
or test("\\p{Han}[,.!?;]|[,.!?;]\\p{Han}")
)
)
| "\($entry.key): \(.)"
' "$catalog")
if [[ -n "$violations" ]]; then
echo "Invalid zh-Hans spacing:" >&2
echo "$violations" >&2
exit 1
fi
echo "zh-Hans localization checks passed."
+30
View File
@@ -4,6 +4,36 @@ import Foundation
import AppKit
#endif
/// Resolves an explicitly requested app language instead of relying on the
/// process-wide bundle negotiation. `String(localized:locale:)` uses `locale`
/// for formatting, but an already-running app (and the test host) can still
/// have a different preferred localization. Loading the matching lproj bundle
/// keeps locale-injectable presentation code deterministic.
nonisolated enum AppLocalization {
static func string(
_ key: String.LocalizationValue, locale: Locale = .current
) -> String {
String(
localized: key,
bundle: bundle(for: locale),
locale: locale
)
}
private static func bundle(for locale: Locale) -> Bundle {
let localization: String? = switch locale.language.languageCode?.identifier {
case "zh": "zh-Hans"
case "en": "en"
default: nil
}
guard let localization,
let path = Bundle.main.path(forResource: localization, ofType: "lproj"),
let bundle = Bundle(path: path)
else { return .main }
return bundle
}
}
nonisolated enum AppLanguage: String, CaseIterable, Identifiable, Sendable {
case system
case english = "en"
@@ -212,23 +212,20 @@ nonisolated extension AgentBuiltInTools {
case "sftp_delete": "trash"
default: "externaldrive.connected.to.line.below"
}
let action: AgentToolDescriptor.Presentation.Action = switch name {
case "sftp_list": .init(infinitive: "list", progressive: "Listing", past: "Listed")
case "sftp_stat": .init(infinitive: "inspect", progressive: "Inspecting", past: "Inspected")
case "sftp_read": .init(infinitive: "read", progressive: "Reading", past: "Read")
case "sftp_write": .init(infinitive: "write", progressive: "Writing", past: "Wrote")
case "sftp_mkdir": .init(infinitive: "create", progressive: "Creating", past: "Created")
case "sftp_move": .init(infinitive: "move", progressive: "Moving", past: "Moved")
case "sftp_chmod": .init(
infinitive: "change permissions for", progressive: "Changing permissions for",
past: "Changed permissions for"
)
case "sftp_delete": .init(infinitive: "delete", progressive: "Deleting", past: "Deleted")
default: .init(infinitive: "use", progressive: "Using", past: "Used")
let actionKind: AgentToolDescriptor.Presentation.ActionKind = switch name {
case "sftp_list": .list
case "sftp_stat": .inspect
case "sftp_read": .read
case "sftp_write": .write
case "sftp_mkdir": .create
case "sftp_move": .move
case "sftp_chmod": .changePermissions
case "sftp_delete": .delete
default: .use
}
return .init(
symbol: symbol, activity: .argument(key: "path", fallback: summary), output: .automatic,
action: action, includesTarget: true
symbol: symbol, activity: .semanticArgument(key: "path", fallback: .remoteFile),
output: .automatic, actionKind: actionKind, includesTarget: true
)
}
@@ -307,4 +304,3 @@ nonisolated extension AgentBuiltInTools {
])
}
}
@@ -7,8 +7,8 @@ nonisolated extension AgentBuiltInTools {
properties: [:], required: [], target: .host, safety: .locallyReadOnly,
concurrency: .parallel,
presentation: .init(
symbol: "server.rack", activity: .label("hosts"), output: .json,
action: .init(infinitive: "list", progressive: "Listing", past: "Listed")
symbol: "server.rack", activity: .semanticLabel(.hosts), output: .json,
actionKind: .list
)
)) { invocation, _ in
let listing = await hosts.listing()
@@ -98,10 +98,8 @@ nonisolated extension AgentBuiltInTools {
concurrency: .parallel,
presentation: .init(
symbol: "clock.arrow.circlepath",
activity: .label("the remote command"), output: .streams,
action: .init(
infinitive: "wait for", progressive: "Waiting for", past: "Waited for"
)
activity: .semanticLabel(.remoteCommand), output: .streams,
actionKind: .wait
)
),
execute: { invocation, context in
@@ -151,8 +149,20 @@ nonisolated extension AgentBuiltInTools {
], required: ["host_id", "command"], target: .host, safety: .requiresAuthorization,
presentation: .init(
symbol: "terminal",
activity: .argument(key: "command", fallback: "the SSH command"),
activity: .semanticArgument(key: "command", fallback: .sshCommand),
output: .streams,
actionKind: .run,
phaseKinds: [
"proposed": .sshPreparingCommand,
"waitingForApproval": .sshWaitingForCommand,
"waitingForUser": .sshWaitingForAuthentication,
"running": .sshConnecting,
"command": .sshRunningCommand,
"completed": .sshCompletedCommand,
"denied": .sshDeniedCommand,
"failed": .sshFailedCommand,
"interrupted": .sshInterruptedCommand,
],
messages: [
"proposed": "Preparing to run “{activity}” on {target}",
"waitingForApproval": "Waiting to run “{activity}” on {target}",
@@ -337,9 +347,9 @@ nonisolated extension AgentBuiltInTools {
// would do if the user ran it is the user's action, later.
concurrency: .parallel,
presentation: .init(
symbol: "terminal", activity: .argument(key: "command", fallback: "a command"),
symbol: "terminal", activity: .semanticArgument(key: "command", fallback: .command),
groupsWithAdjacentTools: false,
action: .init(infinitive: "suggest", progressive: "Suggesting", past: "Suggested")
actionKind: .suggest
)
)) { invocation, _ in
let arguments = try Arguments(invocation)
@@ -57,10 +57,10 @@ nonisolated extension AgentBuiltInTools {
], required: ["questions"], target: .user, safety: .locallyReadOnly,
presentation: .init(
symbol: "questionmark.circle",
activity: .localizedArrayCount(
key: "questions", unit: .question, fallback: "a question"
activity: .semanticArrayCount(
key: "questions", unit: .question, fallback: .question
),
action: .init(infinitive: "ask", progressive: "Asking", past: "Asked")
actionKind: .ask
)
)) { invocation, context in
let arguments = try Arguments(invocation)
@@ -151,8 +151,8 @@ nonisolated extension AgentBuiltInTools {
// means output arriving under a question that is still being read.
target: .user, safety: .locallyReadOnly,
presentation: .init(
symbol: "key", activity: .argument(key: "prompt", fallback: "a secret"),
action: .init(infinitive: "request", progressive: "Requesting", past: "Requested")
symbol: "key", activity: .semanticArgument(key: "prompt", fallback: .secret),
actionKind: .request
)
)) { invocation, context in
let arguments = try Arguments(invocation)
@@ -23,9 +23,9 @@ nonisolated extension AgentBuiltInTools {
// calls pay for themselves.
concurrency: .parallel,
presentation: .init(
symbol: "globe", activity: .argument(key: "url", fallback: "a URL"),
symbol: "globe", activity: .semanticArgument(key: "url", fallback: .url),
output: .field("body"),
action: .init(infinitive: "fetch", progressive: "Fetching", past: "Fetched")
actionKind: .fetch
)
)
return AnyAgentTool(
@@ -116,8 +116,8 @@ nonisolated extension AgentBuiltInTools {
concurrency: .parallel,
presentation: .init(
symbol: "magnifyingglass",
activity: .argument(key: "query", fallback: "the web"), output: .json,
action: .init(infinitive: "search", progressive: "Searching", past: "Searched")
activity: .semanticArgument(key: "query", fallback: .web), output: .json,
actionKind: .search
)
)
return AnyAgentTool(
@@ -152,4 +152,3 @@ nonisolated extension AgentBuiltInTools {
)
}
}
@@ -47,7 +47,7 @@ nonisolated enum AgentMCPTools {
namespace: server.effectiveNamespaceID, function: name
)),
output: .json,
action: .init(infinitive: "run", progressive: "Running", past: "Ran")
actionKind: .run
)
)
return AnyAgentTool(
@@ -76,8 +76,8 @@ nonisolated enum AgentPlanTools: AgentToolSchemaBuilding {
required: ["title", "scratch_path"], target: .user, safety: .locallyReadOnly,
presentation: .init(
symbol: "lightbulb.max",
activity: .argument(key: "title", fallback: "a plan"), output: .field("plan"),
action: .init(infinitive: "present", progressive: "Writing", past: "Presented")
activity: .semanticArgument(key: "title", fallback: .plan), output: .field("plan"),
actionKind: .present
)
)) { invocation, _ in
let arguments = try Arguments(invocation)
@@ -135,9 +135,9 @@ nonisolated enum AgentPlanTools: AgentToolSchemaBuilding {
properties: ["tasks": array(items: task, max: AgentTaskList.maximumTasks)],
required: ["tasks"], target: .user, safety: .locallyReadOnly,
presentation: .init(
symbol: "checklist", activity: .label(String(localized: "the task list")),
symbol: "checklist", activity: .semanticLabel(.taskList),
output: .json,
action: .init(infinitive: "update", progressive: "Updating", past: "Updated")
actionKind: .update
)
)) { invocation, _ in
let updated = try Arguments(invocation).agentTasks()
@@ -58,9 +58,10 @@ nonisolated enum AgentScratchTools: AgentToolSchemaBuilding {
properties: ["path": string(max: pathBytes), "recursive": boolean()],
required: [], target: .local, safety: .locallyReadOnly, concurrency: .parallel,
presentation: .init(
symbol: "folder", activity: .argument(key: "path", fallback: "the scratch workspace"),
symbol: "folder",
activity: .semanticArgument(key: "path", fallback: .scratchWorkspace),
output: .json,
action: .init(infinitive: "list", progressive: "Listing", past: "Listed")
actionKind: .list
)
)) { invocation, _ in
let arguments = try Arguments(invocation)
@@ -101,9 +102,9 @@ nonisolated enum AgentScratchTools: AgentToolSchemaBuilding {
],
required: ["path"], target: .local, safety: .locallyReadOnly, concurrency: .parallel,
presentation: .init(
symbol: "doc.text", activity: .argument(key: "path", fallback: "a scratch file"),
symbol: "doc.text", activity: .semanticArgument(key: "path", fallback: .scratchFile),
output: .field("content"),
action: .init(infinitive: "read", progressive: "Reading", past: "Read")
actionKind: .read
)
)) { invocation, _ in
let arguments = try Arguments(invocation)
@@ -137,8 +138,8 @@ nonisolated enum AgentScratchTools: AgentToolSchemaBuilding {
concurrency: .parallel,
presentation: .init(
symbol: "square.and.pencil",
activity: .argument(key: "path", fallback: "a scratch file"), output: .json,
action: .init(infinitive: "write", progressive: "Writing", past: "Wrote")
activity: .semanticArgument(key: "path", fallback: .scratchFile), output: .json,
actionKind: .write
)
)) { invocation, _ in
let arguments = try Arguments(invocation)
@@ -185,9 +186,9 @@ nonisolated enum AgentScratchTools: AgentToolSchemaBuilding {
required: ["path", "edits"], target: .local, safety: .locallyContained,
concurrency: .parallel,
presentation: .init(
symbol: "pencil", activity: .argument(key: "path", fallback: "a scratch file"),
symbol: "pencil", activity: .semanticArgument(key: "path", fallback: .scratchFile),
output: .field("diff"),
action: .init(infinitive: "edit", progressive: "Editing", past: "Edited")
actionKind: .edit
)
)) { invocation, _ in
let arguments = try Arguments(invocation)
@@ -226,8 +227,8 @@ nonisolated enum AgentScratchTools: AgentToolSchemaBuilding {
concurrency: .parallel,
presentation: .init(
symbol: "arrow.left.arrow.right",
activity: .argument(key: "path_a", fallback: "scratch files"), output: .field("diff"),
action: .init(infinitive: "compare", progressive: "Comparing", past: "Compared")
activity: .semanticArgument(key: "path_a", fallback: .scratchFiles),
output: .field("diff"), actionKind: .compare
)
)) { invocation, _ in
let arguments = try Arguments(invocation)
@@ -264,9 +265,9 @@ nonisolated enum AgentScratchTools: AgentToolSchemaBuilding {
required: ["path", "kind"], target: .local, safety: .locallyContained,
concurrency: .parallel,
presentation: .init(
symbol: "trash", activity: .argument(key: "path", fallback: "a scratch entry"),
symbol: "trash", activity: .semanticArgument(key: "path", fallback: .scratchEntry),
output: .json,
action: .init(infinitive: "delete", progressive: "Deleting", past: "Deleted")
actionKind: .delete
)
)) { invocation, _ in
let arguments = try Arguments(invocation)
@@ -311,8 +312,8 @@ nonisolated enum AgentScratchTools: AgentToolSchemaBuilding {
safety: .locallyContained, concurrency: .parallel,
presentation: .init(
symbol: "arrow.down.doc",
activity: .argument(key: "remote_path", fallback: "a remote file"), output: .json,
action: .init(infinitive: "pull", progressive: "Pulling", past: "Pulled"),
activity: .semanticArgument(key: "remote_path", fallback: .remoteFile),
output: .json, actionKind: .pull,
includesTarget: true
)
)
@@ -380,8 +381,8 @@ nonisolated enum AgentScratchTools: AgentToolSchemaBuilding {
safety: .requiresAuthorization,
presentation: .init(
symbol: "arrow.up.doc",
activity: .argument(key: "remote_path", fallback: "a remote file"), output: .json,
action: .init(infinitive: "push", progressive: "Pushing", past: "Pushed"),
activity: .semanticArgument(key: "remote_path", fallback: .remoteFile),
output: .json, actionKind: .push,
includesTarget: true
)
)
@@ -15,6 +15,15 @@ nonisolated enum AgentApprovalDecision: Hashable, Sendable {
nonisolated enum SecurityReviewRisk: String, Hashable, Sendable, Codable {
case low, medium, high, critical
var displayName: String {
switch self {
case .low: String(localized: "Low")
case .medium: String(localized: "Medium")
case .high: String(localized: "High")
case .critical: String(localized: "Critical")
}
}
}
nonisolated enum SecurityReviewVerdict: String, Hashable, Sendable, Codable {
@@ -191,15 +191,14 @@ nonisolated struct AgentTurnDriver: Sendable {
concurrency: .parallel,
presentation: .init(
symbol: "magnifyingglass",
activity: .argument(key: "query", fallback: "the web"), output: .json,
action: .init(infinitive: "search", progressive: "Searching", past: "Searched")
activity: .semanticArgument(key: "query", fallback: .web), output: .json,
actionKind: .search
)
)
let sources = AgentJSONValue.array(activity.sources.map { source in
.object(["title": .string(source.title), "url": .string(source.url)])
})
channel.emit(.toolProposed(invocation, descriptor))
channel.emit(.toolFinished(invocation, AgentToolResult(
var result = AgentToolResult(
callID: invocation.call.id,
content: sources.encodedString,
// The flag every reader of this event needs: `callID` here names
@@ -207,6 +206,13 @@ nonisolated struct AgentTurnDriver: Sendable {
// results must leave this one out. See
// `AgentToolResult.isProviderNative`.
metadata: [AgentToolResult.providerNativeKey: .bool(true)]
)))
)
// Native searches bypass AgentToolExecutor, whose normal finish path
// persists presentation metadata. Store it here as well so a restored
// card can still derive its activity from `query` instead of exposing
// the internal `web_search` protocol name.
result.toolPresentation = descriptor.presentation
channel.emit(.toolProposed(invocation, descriptor))
channel.emit(.toolFinished(invocation, result))
}
}
+59 -1
View File
@@ -72,6 +72,30 @@ nonisolated enum AgentJSONValue: Hashable, Sendable, Codable {
nonisolated struct AgentToolDescriptor: Identifiable, Hashable, Sendable, Codable {
nonisolated struct Presentation: Hashable, Sendable, Codable {
/// Stable application-owned semantics. Display strings are derived at
/// render time so a persisted card follows the viewer's current locale
/// instead of freezing whichever English template created it.
nonisolated enum ActionKind: String, Hashable, Sendable, Codable {
case ask, compare, create, delete, edit, fetch, inspect, list, move
case present, pull, push, read, request, run, search, suggest, update
case wait, write, changePermissions, use
}
nonisolated enum ActivityKind: String, Hashable, Sendable, Codable {
case command, hosts, plan, question, remoteCommand, remoteFile, scratchEntry
case scratchFile, scratchFiles, scratchWorkspace, secret, sshCommand, taskList
case url, web
}
/// Semantic overrides for phases whose grammar is more specific than
/// the ordinary action lifecycle. Currently SSH is the only built-in
/// with connection and authentication phases of its own.
nonisolated enum PhaseKind: String, Hashable, Sendable, Codable {
case sshPreparingCommand, sshWaitingForCommand, sshWaitingForAuthentication
case sshConnecting, sshRunningCommand, sshCompletedCommand
case sshDeniedCommand, sshFailedCommand, sshInterruptedCommand
}
nonisolated struct Action: Hashable, Sendable, Codable {
var infinitive: String
var progressive: String
@@ -87,9 +111,15 @@ nonisolated struct AgentToolDescriptor: Identifiable, Hashable, Sendable, Codabl
nonisolated enum Activity: Hashable, Sendable, Codable {
case label(String)
case argument(key: String, fallback: String)
/// Persisted by releases before counted activities became
/// semantic. Keep decoding it so old conversations remain usable.
case arrayCount(key: String, singular: String, plural: String, fallback: String)
/// Names an activity from the number of entries in an array
/// argument. The unit selects a locale-aware plural resource.
case localizedArrayCount(key: String, unit: CountedUnit, fallback: String)
case semanticLabel(ActivityKind)
case semanticArgument(key: String, fallback: ActivityKind)
case semanticArrayCount(key: String, unit: CountedUnit, fallback: ActivityKind)
}
nonisolated enum Output: Hashable, Sendable, Codable {
@@ -105,6 +135,8 @@ nonisolated struct AgentToolDescriptor: Identifiable, Hashable, Sendable, Codabl
/// Defaults to participating. A tool may opt out when its row is
/// proposed content rather than work that belongs in an activity batch.
var groupsWithAdjacentTools: Bool = true
var actionKind: ActionKind? = nil
var phaseKinds: [String: PhaseKind] = [:]
/// Templates keyed by card state or a tool-reported live phase.
/// Supported placeholders are `{activity}` and `{target}`.
var messages: [String: String] = [:]
@@ -112,13 +144,17 @@ nonisolated struct AgentToolDescriptor: Identifiable, Hashable, Sendable, Codabl
init(
symbol: String, activity: Activity, output: Output = .automatic,
groupsWithAdjacentTools: Bool = true,
action: Action? = nil, includesTarget: Bool = false,
action: Action? = nil, actionKind: ActionKind? = nil,
includesTarget: Bool = false,
phaseKinds: [String: PhaseKind] = [:],
messages overrides: [String: String] = [:]
) {
self.symbol = symbol
self.activity = activity
self.output = output
self.groupsWithAdjacentTools = groupsWithAdjacentTools
self.actionKind = actionKind
self.phaseKinds = phaseKinds
let target = includesTarget ? " on {target}" : ""
if let action {
messages = [
@@ -135,6 +171,28 @@ nonisolated struct AgentToolDescriptor: Identifiable, Hashable, Sendable, Codabl
messages.merge(overrides) { _, override in override }
}
private enum CodingKeys: String, CodingKey {
case symbol, activity, output, groupsWithAdjacentTools, messages
case actionKind, phaseKinds
}
init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
symbol = try container.decode(String.self, forKey: .symbol)
activity = try container.decode(Activity.self, forKey: .activity)
output = try container.decodeIfPresent(Output.self, forKey: .output) ?? .automatic
groupsWithAdjacentTools = try container.decodeIfPresent(
Bool.self, forKey: .groupsWithAdjacentTools
) ?? true
messages = try container.decodeIfPresent(
[String: String].self, forKey: .messages
) ?? [:]
actionKind = try container.decodeIfPresent(ActionKind.self, forKey: .actionKind)
phaseKinds = try container.decodeIfPresent(
[String: PhaseKind].self, forKey: .phaseKinds
) ?? [:]
}
}
nonisolated enum Target: String, Hashable, Sendable, Codable {
+706 -55
View File
@@ -130,9 +130,10 @@ nonisolated enum AgentToolPresentation {
static func summary(
for card: AgentToolCard,
hostName: String? = nil
hostName: String? = nil,
locale: Locale = .current
) -> String {
let operation = operationName(for: card)
let operation = operation(for: card, locale: locale)
let phase: String
if card.state == .running {
phase = card.result?.presentationPhase ?? card.state.rawValue
@@ -141,8 +142,20 @@ nonisolated enum AgentToolPresentation {
} else {
phase = card.state.rawValue
}
if let phaseKind = phaseKind(for: card, phase: phase) {
return phaseSummary(
phaseKind, activity: operation.text, target: hostName, locale: locale
)
}
if let actionKind = actionKind(for: card) {
return actionSummary(
actionKind, state: card.state, activity: operation.text,
isArgumentValue: operation.isArgumentValue,
target: hostName, locale: locale
)
}
if let template = card.descriptor.presentation?.messages[phase] {
var rendered = template.replacingOccurrences(of: "{activity}", with: operation)
var rendered = template.replacingOccurrences(of: "{activity}", with: operation.text)
if let hostName, !hostName.isEmpty {
rendered = rendered.replacingOccurrences(of: "{target}", with: hostName)
} else {
@@ -159,18 +172,22 @@ nonisolated enum AgentToolPresentation {
let action: String
switch card.state {
case .proposed: action = String(localized: "Preparing \(operation)")
case .waitingForApproval: action = String(localized: "Waiting to run \(operation)")
case .waitingForUser: action = String(localized: "Waiting to run \(operation)")
case .reviewing: action = String(localized: "Preparing \(operation)")
case .running: action = String(localized: "Running \(operation)")
case .completed: action = String(localized: "Ran \(operation)")
case .denied: action = String(localized: "Denied \(operation)")
case .failed: action = String(localized: "Failed \(operation)")
case .interrupted: action = String(localized: "Interrupted \(operation)")
case .proposed: action = localized("Preparing \(operation.text)", locale: locale)
case .waitingForApproval:
action = localized("Waiting to run \(operation.text)", locale: locale)
case .waitingForUser:
action = localized("Waiting to run \(operation.text)", locale: locale)
case .reviewing: action = localized("Preparing \(operation.text)", locale: locale)
case .running: action = localized("Running \(operation.text)", locale: locale)
case .completed: action = localized("Ran \(operation.text)", locale: locale)
case .denied: action = localized("Denied \(operation.text)", locale: locale)
case .failed: action = localized("Failed \(operation.text)", locale: locale)
case .interrupted: action = localized("Interrupted \(operation.text)", locale: locale)
}
if let hostName, !hostName.isEmpty { return String(localized: "\(action) on \(hostName)") }
if let hostName, !hostName.isEmpty {
return localized("\(action) on \(hostName)", locale: locale)
}
return action
}
@@ -199,7 +216,9 @@ nonisolated enum AgentToolPresentation {
/// metadata as its individual rows but with repeated activities counted.
/// Calls in different terminal states stay separate, so one failure can
/// never be rewritten as a successful action by the batch title.
static func batchSummary(for cards: [AgentToolCard]) -> String {
static func batchSummary(
for cards: [AgentToolCard], locale: Locale = .current
) -> String {
struct Key: Hashable {
let name: String
let state: AgentToolCard.State
@@ -212,8 +231,13 @@ nonisolated enum AgentToolPresentation {
if grouped[key] == nil { order.append(key) }
grouped[key, default: []].append(card)
}
return order.compactMap { grouped[$0].map(batchClause) }
.enumerated()
let clauses = order.compactMap { key in
grouped[key].map { batchClause($0, locale: locale) }
}
if locale.language.languageCode?.identifier == "zh" {
return clauses.joined(separator: ",")
}
return clauses.enumerated()
.map { index, clause in index == 0 ? clause : lowercaseFirst(clause) }
.joined(separator: ", ")
}
@@ -479,7 +503,7 @@ nonisolated enum AgentToolPresentation {
?? nonempty(path.split(separator: "/").last.map(String.init))
?? String(localized: "Unnamed item")
let kind = object["kind"]?.stringValue ?? ""
var metadata = [humanized(kind).capitalized]
var metadata = [kindLabel(kind)]
if let bytes = object["size"]?.integerValue { metadata.append(byteCount(bytes)) }
if let permissions = nonempty(object["permissions"]?.stringValue) {
metadata.append(permissions)
@@ -495,7 +519,7 @@ nonisolated enum AgentToolPresentation {
guard let object = value.objectValue else { return genericItems(value) }
var items: [DetailItem] = []
appendField(&items, "Path", object["path"]?.stringValue, monospaced: true)
appendField(&items, "Kind", object["kind"]?.stringValue.map { humanized($0).capitalized })
appendField(&items, "Kind", object["kind"]?.stringValue.map(kindLabel))
if let bytes = object["size"]?.integerValue { items.append(field("Size", byteCount(bytes))) }
appendField(&items, "Permissions", object["permissions"]?.stringValue, monospaced: true)
appendField(&items, "Modified", object["modified_at"]?.stringValue.map(displayDate))
@@ -559,7 +583,7 @@ nonisolated enum AgentToolPresentation {
guard let object = value.objectValue else { return genericItems(value) }
var items: [DetailItem] = []
appendField(&items, "Path", object["path"]?.stringValue, monospaced: true)
appendField(&items, "Kind", arguments["kind"]?.stringValue.map { humanized($0).capitalized })
appendField(&items, "Kind", arguments["kind"]?.stringValue.map(kindLabel))
items.append(field("Result", String(localized: "Deleted")))
return items
}
@@ -702,7 +726,7 @@ nonisolated enum AgentToolPresentation {
guard let fields = entry.objectValue else { return nil }
let path = fields["path"]?.stringValue ?? String(localized: "Unnamed entry")
let kind = fields["kind"]?.stringValue ?? ""
var subtitle = humanized(kind).capitalized
var subtitle = kindLabel(kind)
if let bytes = fields["bytes"]?.integerValue { subtitle += " · \(byteCount(bytes))" }
return DetailListRow(
title: path, subtitle: subtitle,
@@ -793,7 +817,7 @@ nonisolated enum AgentToolPresentation {
private static func scratchDelete(_ value: AgentJSONValue) -> [DetailItem] {
guard let object = value.objectValue else { return genericItems(value) }
var items = pathResult(value, result: String(localized: "Deleted"))
appendField(&items, "Kind", object["kind"]?.stringValue.map { humanized($0).capitalized })
appendField(&items, "Kind", object["kind"]?.stringValue.map(kindLabel))
let count = object["entry_count"]?.integerValue
let bytes = object["total_bytes"]?.integerValue
if count != nil || bytes != nil {
@@ -945,6 +969,15 @@ nonisolated enum AgentToolPresentation {
}
}
private static func kindLabel(_ kind: String) -> String {
switch kind {
case "file": String(localized: "File")
case "directory": String(localized: "Directory")
case "symlink", "symbolic_link": String(localized: "Symlink")
default: humanized(kind).capitalized
}
}
private static func hostCount(_ count: Int) -> String {
String(localized: "\(count) hosts")
}
@@ -1084,46 +1117,238 @@ nonisolated enum AgentToolPresentation {
return prettyJSON(value)
}
private static func operationName(for card: AgentToolCard) -> String {
private struct RenderedOperation {
var text: String
var isArgumentValue: Bool
}
private static func operation(
for card: AgentToolCard, locale: Locale
) -> RenderedOperation {
guard let presentation = card.descriptor.presentation else {
return humanized(card.descriptor.qualifiedName)
if isChinese(locale), let recovered = recoveredBuiltInOperation(
for: card, locale: locale
) {
return recovered
}
return RenderedOperation(
text: humanized(card.descriptor.qualifiedName), isArgumentValue: false
)
}
switch presentation.activity {
case .label(let label): return label
case .label(let label):
return RenderedOperation(
text: legacyActivity(
label, descriptorName: card.descriptor.name, locale: locale
),
isArgumentValue: false
)
case .argument(let key, let fallback):
guard let value = card.invocation.call.arguments.objectValue?[key]?.stringValue else {
return fallback
return RenderedOperation(
text: legacyActivity(
fallback, descriptorName: card.descriptor.name, locale: locale
),
isArgumentValue: false
)
}
let oneLine = value.split(whereSeparator: \.isNewline).joined(separator: " ")
return oneLine.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
? fallback : oneLine
guard !oneLine.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
return RenderedOperation(
text: legacyActivity(
fallback, descriptorName: card.descriptor.name, locale: locale
),
isArgumentValue: false
)
}
return RenderedOperation(text: oneLine, isArgumentValue: true)
case .arrayCount(let key, let singular, let plural, let fallback):
guard let count = card.invocation.call.arguments.objectValue?[key]?.arrayValue?.count,
count > 0
else {
return RenderedOperation(
text: legacyActivity(
fallback, descriptorName: card.descriptor.name, locale: locale
),
isArgumentValue: false
)
}
if card.descriptor.name == "request_user_input" {
return RenderedOperation(
text: countDescription(count, unit: .question, locale: locale),
isArgumentValue: false
)
}
return RenderedOperation(
text: count == 1 ? singular : "\(count) \(plural)", isArgumentValue: false
)
case .localizedArrayCount(let key, let unit, let fallback):
guard let count = card.invocation.call.arguments.objectValue?[key]?.arrayValue?.count,
count > 0
else { return fallback }
return countDescription(count, unit: unit)
else {
return RenderedOperation(
text: legacyActivity(
fallback, descriptorName: card.descriptor.name, locale: locale
),
isArgumentValue: false
)
}
return RenderedOperation(
text: countDescription(count, unit: unit, locale: locale),
isArgumentValue: false
)
case .semanticLabel(let kind):
return RenderedOperation(
text: activityName(kind, locale: locale), isArgumentValue: false
)
case .semanticArgument(let key, let fallback):
guard let value = card.invocation.call.arguments.objectValue?[key]?.stringValue else {
if card.descriptor.name == "web_search" {
return RenderedOperation(text: "", isArgumentValue: false)
}
return RenderedOperation(
text: activityName(fallback, locale: locale), isArgumentValue: false
)
}
let oneLine = value.split(whereSeparator: \.isNewline).joined(separator: " ")
guard !oneLine.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
if card.descriptor.name == "web_search" {
return RenderedOperation(text: "", isArgumentValue: false)
}
return RenderedOperation(
text: activityName(fallback, locale: locale), isArgumentValue: false
)
}
return RenderedOperation(text: oneLine, isArgumentValue: true)
case .semanticArrayCount(let key, let unit, let fallback):
guard let count = card.invocation.call.arguments.objectValue?[key]?.arrayValue?.count,
count > 0
else {
return RenderedOperation(
text: activityName(fallback, locale: locale), isArgumentValue: false
)
}
return RenderedOperation(
text: countDescription(count, unit: unit, locale: locale),
isArgumentValue: false
)
}
}
private static func batchClause(_ cards: [AgentToolCard]) -> String {
/// Cards persisted before semantic presentations were stored still know
/// their tool name and arguments. Recover the same user-facing activity
/// from those stable protocol fields instead of exposing names such as
/// `web_search` in a translated summary.
private static func recoveredBuiltInOperation(
for card: AgentToolCard, locale: Locale
) -> RenderedOperation? {
let semanticArgument: (key: String, fallback: AgentToolDescriptor.Presentation.ActivityKind)? =
switch card.descriptor.name {
case "fetch": ("url", .url)
case "web_search": ("query", .web)
case "ssh_exec", "terminal_suggest": ("command", .command)
case "sftp_list", "sftp_stat", "sftp_read", "sftp_write", "sftp_mkdir",
"sftp_move", "sftp_chmod", "sftp_delete":
("path", .remoteFile)
case "request_user_secret": ("prompt", .secret)
case "scratch_list": ("path", .scratchWorkspace)
case "scratch_read", "scratch_write", "scratch_edit": ("path", .scratchFile)
case "scratch_diff": ("path_a", .scratchFiles)
case "scratch_delete": ("path", .scratchEntry)
case "scratch_pull", "scratch_push": ("remote_path", .remoteFile)
case "present_plan": ("title", .plan)
default: nil
}
if let semanticArgument {
if let value = card.invocation.call.arguments.objectValue?[semanticArgument.key]?
.stringValue
{
let oneLine = value.split(whereSeparator: \.isNewline).joined(separator: " ")
if !oneLine.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
return RenderedOperation(text: oneLine, isArgumentValue: true)
}
}
if card.descriptor.name == "web_search" {
return RenderedOperation(text: "", isArgumentValue: false)
}
return RenderedOperation(
text: activityName(semanticArgument.fallback, locale: locale),
isArgumentValue: false
)
}
let semanticLabel: AgentToolDescriptor.Presentation.ActivityKind? =
switch card.descriptor.name {
case "list_hosts": .hosts
case "ssh_wait": .remoteCommand
case "manage_tasks": .taskList
default: nil
}
if let semanticLabel {
return RenderedOperation(
text: activityName(semanticLabel, locale: locale), isArgumentValue: false
)
}
if card.descriptor.name == "request_user_input" {
let count = card.invocation.call.arguments.objectValue?["questions"]?.arrayValue?.count
return RenderedOperation(
text: count.map { countDescription($0, unit: .question, locale: locale) }
?? activityName(.question, locale: locale),
isArgumentValue: false
)
}
return nil
}
private static func batchClause(
_ cards: [AgentToolCard], locale: Locale
) -> String {
guard let first = cards.first else { return "" }
guard let presentation = first.descriptor.presentation else {
let activity = cards.count == 1
? humanized(first.descriptor.qualifiedName) : toolCallCount(cards.count)
return fallbackAction(for: first.state, activity: activity)
? operation(for: first, locale: locale)
: RenderedOperation(
text: toolCallCount(cards.count, locale: locale), isArgumentValue: false
)
if let actionKind = actionKind(for: first) {
if isChinese(locale), cards.count > 1 {
return repeatedActionSummary(
actionKind, state: first.state, count: cards.count, locale: locale
)
}
return actionSummary(
actionKind, state: first.state, activity: activity.text,
isArgumentValue: activity.isArgumentValue,
target: nil, locale: locale
)
}
return fallbackAction(for: first.state, activity: activity.text, locale: locale)
}
let activity = batchActivity(presentation.activity, cards: cards)
let activity = batchActivity(presentation.activity, cards: cards, locale: locale)
if let actionKind = actionKind(for: first) {
if isChinese(locale), cards.count > 1 {
return repeatedActionSummary(
actionKind, state: first.state, count: cards.count, locale: locale
)
}
return actionSummary(
actionKind, state: first.state, activity: activity.text,
isArgumentValue: activity.isArgumentValue,
target: nil, locale: locale
)
}
if let template = presentation.messages[first.state.rawValue] {
return replacingQuotedActivity(in: template, with: activity)
.replacingOccurrences(of: "{activity}", with: activity)
return replacingQuotedActivity(in: template, with: activity.text)
.replacingOccurrences(of: "{activity}", with: activity.text)
.replacingOccurrences(of: " on {target}", with: "")
.replacingOccurrences(of: " to {target}", with: "")
.replacingOccurrences(of: " with {target}", with: "")
.replacingOccurrences(of: "{target}", with: "")
.trimmingCharacters(in: .whitespacesAndNewlines)
}
return fallbackAction(for: first.state, activity: activity)
return fallbackAction(for: first.state, activity: activity.text, locale: locale)
}
private static func replacingQuotedActivity(in template: String, with activity: String) -> String {
@@ -1137,49 +1362,475 @@ nonisolated enum AgentToolPresentation {
private static func batchActivity(
_ activity: AgentToolDescriptor.Presentation.Activity,
cards: [AgentToolCard]
) -> String {
cards: [AgentToolCard], locale: Locale
) -> RenderedOperation {
switch activity {
case .localizedArrayCount(let key, let unit, let fallback):
let count = cards.reduce(0) { total, card in
total + (card.invocation.call.arguments.objectValue?[key]?.arrayValue?.count ?? 0)
}
guard count > 0 else { return fallback }
return countDescription(count, unit: unit)
guard count > 0 else {
return RenderedOperation(text: fallback, isArgumentValue: false)
}
return RenderedOperation(
text: countDescription(count, unit: unit, locale: locale),
isArgumentValue: false
)
case .semanticArrayCount(let key, let unit, let fallback):
let count = cards.reduce(0) { total, card in
total + (card.invocation.call.arguments.objectValue?[key]?.arrayValue?.count ?? 0)
}
guard count > 0 else {
return RenderedOperation(
text: activityName(fallback, locale: locale), isArgumentValue: false
)
}
return RenderedOperation(
text: countDescription(count, unit: unit, locale: locale),
isArgumentValue: false
)
case .arrayCount(let key, _, _, let fallback):
let count = cards.reduce(0) { total, card in
total + (card.invocation.call.arguments.objectValue?[key]?.arrayValue?.count ?? 0)
}
guard count > 0 else {
return RenderedOperation(text: fallback, isArgumentValue: false)
}
if cards.first?.descriptor.name == "request_user_input" {
return RenderedOperation(
text: countDescription(count, unit: .question, locale: locale),
isArgumentValue: false
)
}
return RenderedOperation(
text: cards.count == 1 ? fallback : toolCallCount(cards.count, locale: locale),
isArgumentValue: false
)
case .argument(_, let fallback):
return cards.count == 1 ? fallback : toolCallCount(cards.count)
return cards.count == 1
? RenderedOperation(
text: legacyActivity(
fallback, descriptorName: cards[0].descriptor.name, locale: locale
),
isArgumentValue: false
)
: RenderedOperation(
text: toolCallCount(cards.count, locale: locale), isArgumentValue: false
)
case .label(let label):
return cards.count == 1 ? label : toolCallCount(cards.count)
return cards.count == 1
? RenderedOperation(
text: legacyActivity(
label, descriptorName: cards[0].descriptor.name, locale: locale
),
isArgumentValue: false
)
: RenderedOperation(
text: toolCallCount(cards.count, locale: locale), isArgumentValue: false
)
case .semanticArgument(_, let fallback):
if cards.count == 1, cards[0].descriptor.name == "web_search" {
return operation(for: cards[0], locale: locale)
}
return RenderedOperation(
text: cards.count == 1
? activityName(fallback, locale: locale)
: toolCallCount(cards.count, locale: locale),
isArgumentValue: false
)
case .semanticLabel(let kind):
return RenderedOperation(
text: cards.count == 1
? activityName(kind, locale: locale)
: toolCallCount(cards.count, locale: locale),
isArgumentValue: false
)
}
}
private static func countDescription(
_ count: Int, unit: AgentToolDescriptor.Presentation.CountedUnit
_ count: Int, unit: AgentToolDescriptor.Presentation.CountedUnit,
locale: Locale
) -> String {
switch unit {
case .question: String(localized: "\(count) questions")
case .question: localized("\(count) questions", locale: locale)
}
}
private static func toolCallCount(_ count: Int) -> String {
String(localized: "\(count) tool calls")
private static func toolCallCount(_ count: Int, locale: Locale) -> String {
localized("\(count) tool calls", locale: locale)
}
private static func fallbackAction(
for state: AgentToolCard.State, activity: String
for state: AgentToolCard.State, activity: String, locale: Locale
) -> String {
switch state {
case .proposed: "Preparing \(activity)"
case .waitingForApproval, .waitingForUser: "Waiting to run \(activity)"
case .reviewing: "Preparing \(activity)"
case .running: "Running \(activity)"
case .completed: "Ran \(activity)"
case .denied: "Denied \(activity)"
case .failed: "Failed \(activity)"
case .interrupted: "Interrupted \(activity)"
case .proposed: localized("Preparing \(activity)", locale: locale)
case .waitingForApproval, .waitingForUser:
localized("Waiting to run \(activity)", locale: locale)
case .reviewing: localized("Preparing \(activity)", locale: locale)
case .running: localized("Running \(activity)", locale: locale)
case .completed: localized("Ran \(activity)", locale: locale)
case .denied: localized("Denied \(activity)", locale: locale)
case .failed: localized("Failed \(activity)", locale: locale)
case .interrupted: localized("Interrupted \(activity)", locale: locale)
}
}
private enum ActionForm { case infinitive, progressive, past }
private static func actionKind(
for card: AgentToolCard
) -> AgentToolDescriptor.Presentation.ActionKind? {
if let kind = card.descriptor.presentation?.actionKind { return kind }
return switch card.descriptor.name {
case "fetch": .fetch
case "web_search": .search
case "list_hosts", "sftp_list", "scratch_list": .list
case "ssh_wait": .wait
case "ssh_exec": .run
case "terminal_suggest": .suggest
case "sftp_stat": .inspect
case "sftp_read", "scratch_read": .read
case "sftp_write", "scratch_write": .write
case "sftp_mkdir": .create
case "sftp_move": .move
case "sftp_chmod": .changePermissions
case "sftp_delete", "scratch_delete": .delete
case "request_user_input": .ask
case "request_user_secret": .request
case "scratch_edit": .edit
case "scratch_diff": .compare
case "scratch_pull": .pull
case "scratch_push": .push
case "present_plan": .present
case "manage_tasks": .update
default: card.descriptor.target == .mcp ? .run : nil
}
}
private static func phaseKind(
for card: AgentToolCard, phase: String
) -> AgentToolDescriptor.Presentation.PhaseKind? {
if let kind = card.descriptor.presentation?.phaseKinds[phase] { return kind }
guard card.descriptor.name == "ssh_exec" else { return nil }
return switch phase {
case "proposed": .sshPreparingCommand
case "waitingForApproval": .sshWaitingForCommand
case "waitingForUser": .sshWaitingForAuthentication
case "running": .sshConnecting
case "command": .sshRunningCommand
case "completed": .sshCompletedCommand
case "denied": .sshDeniedCommand
case "failed": .sshFailedCommand
case "interrupted": .sshInterruptedCommand
default: nil
}
}
private static func actionSummary(
_ kind: AgentToolDescriptor.Presentation.ActionKind,
state: AgentToolCard.State, activity: String, isArgumentValue: Bool,
target: String?, locale: Locale
) -> String {
if isChinese(locale) {
let action = localizedActionVerb(kind, locale: locale)
let displayedActivity = isArgumentValue ? "“\(activity)”" : activity
let rendered: String = switch state {
case .proposed, .reviewing:
localized("Tool action preparing: \(action) \(displayedActivity)", locale: locale)
case .waitingForApproval, .waitingForUser:
localized("Tool action waiting: \(action) \(displayedActivity)", locale: locale)
case .running:
localized("Tool action running: \(action) \(displayedActivity)", locale: locale)
case .completed:
localized("Tool action completed: \(action) \(displayedActivity)", locale: locale)
case .denied:
localized("Tool action denied: \(action) \(displayedActivity)", locale: locale)
case .failed:
localized("Tool action failed: \(action) \(displayedActivity)", locale: locale)
case .interrupted:
localized("Tool action interrupted: \(action) \(displayedActivity)", locale: locale)
}
guard let target, !target.isEmpty else { return rendered }
return localized("Tool action target: \(rendered) \(target)", locale: locale)
}
if activity.isEmpty {
let rendered: String = switch state {
case .proposed, .reviewing:
"Preparing to \(actionName(kind, form: .infinitive, locale: locale))"
case .waitingForApproval, .waitingForUser:
"Waiting to \(actionName(kind, form: .infinitive, locale: locale))"
case .running: actionName(kind, form: .progressive, locale: locale)
case .completed: actionName(kind, form: .past, locale: locale)
case .denied:
"Did not \(actionName(kind, form: .infinitive, locale: locale))"
case .failed:
"Failed to \(actionName(kind, form: .infinitive, locale: locale))"
case .interrupted:
"Interrupted while "
+ lowercaseFirst(actionName(kind, form: .progressive, locale: locale))
}
guard let target, !target.isEmpty else { return rendered }
return "\(rendered) on \(target)"
}
let rendered: String
switch state {
case .proposed, .reviewing:
rendered = "Preparing to \(actionName(kind, form: .infinitive, locale: locale)) \(activity)"
case .waitingForApproval, .waitingForUser:
rendered = "Waiting to \(actionName(kind, form: .infinitive, locale: locale)) \(activity)"
case .running:
rendered = "\(actionName(kind, form: .progressive, locale: locale)) \(activity)"
case .completed:
rendered = "\(actionName(kind, form: .past, locale: locale)) \(activity)"
case .denied:
rendered = "Did not \(actionName(kind, form: .infinitive, locale: locale)) \(activity)"
case .failed:
rendered = "Failed to \(actionName(kind, form: .infinitive, locale: locale)) \(activity)"
case .interrupted:
let action = lowercaseFirst(actionName(kind, form: .progressive, locale: locale))
rendered = "Interrupted while \(action) \(activity)"
}
guard let target, !target.isEmpty else { return rendered }
return "\(rendered) on \(target)"
}
private static func repeatedActionSummary(
_ kind: AgentToolDescriptor.Presentation.ActionKind,
state: AgentToolCard.State, count: Int, locale: Locale
) -> String {
let action = localizedActionVerb(kind, locale: locale)
switch state {
case .failed:
return localized("Repeated tool action failed: \(count) \(action)", locale: locale)
case .interrupted:
return localized(
"Repeated tool action interrupted: \(count) \(action)", locale: locale
)
default:
return actionSummary(
kind, state: state, activity: localized("\(count) times", locale: locale),
isArgumentValue: false, target: nil, locale: locale
)
}
}
private static func phaseSummary(
_ kind: AgentToolDescriptor.Presentation.PhaseKind,
activity: String, target: String?, locale: Locale
) -> String {
switch kind {
case .sshPreparingCommand:
return target.map {
localized("Preparing to run “\(activity)” on \($0)", locale: locale)
} ?? localized("Preparing to run “\(activity)”", locale: locale)
case .sshWaitingForCommand:
return target.map {
localized("Waiting to run “\(activity)” on \($0)", locale: locale)
} ?? localized("Waiting to run “\(activity)”", locale: locale)
case .sshWaitingForAuthentication:
return target.map {
localized("Waiting to authenticate with \($0)", locale: locale)
} ?? localized("Waiting for authentication", locale: locale)
case .sshConnecting:
return target.map {
localized("Connecting to \($0)", locale: locale)
} ?? localized("Connecting", locale: locale)
case .sshRunningCommand:
return target.map {
localized("Running “\(activity)” on \($0)", locale: locale)
} ?? localized("Running “\(activity)”", locale: locale)
case .sshCompletedCommand:
return target.map {
localized("Ran “\(activity)” on \($0)", locale: locale)
} ?? localized("Ran “\(activity)”", locale: locale)
case .sshDeniedCommand:
return target.map {
localized("Did not run “\(activity)” on \($0)", locale: locale)
} ?? localized("Did not run “\(activity)”", locale: locale)
case .sshFailedCommand:
return target.map {
localized("Failed to run “\(activity)” on \($0)", locale: locale)
} ?? localized("Failed to run “\(activity)”", locale: locale)
case .sshInterruptedCommand:
return target.map {
localized("Stopped waiting for “\(activity)” on \($0)", locale: locale)
} ?? localized("Stopped waiting for “\(activity)”", locale: locale)
}
}
private static func actionName(
_ kind: AgentToolDescriptor.Presentation.ActionKind,
form: ActionForm, locale: Locale
) -> String {
if isChinese(locale) { return localizedActionVerb(kind, locale: locale) }
return switch (kind, form) {
case (.ask, .infinitive): "ask"
case (.ask, .progressive): "Asking"
case (.ask, .past): "Asked"
case (.compare, .infinitive): "compare"
case (.compare, .progressive): "Comparing"
case (.compare, .past): "Compared"
case (.create, .infinitive): "create"
case (.create, .progressive): "Creating"
case (.create, .past): "Created"
case (.delete, .infinitive): "delete"
case (.delete, .progressive): "Deleting"
case (.delete, .past): "Deleted"
case (.edit, .infinitive): "edit"
case (.edit, .progressive): "Editing"
case (.edit, .past): "Edited"
case (.fetch, .infinitive): "fetch"
case (.fetch, .progressive): "Fetching"
case (.fetch, .past): "Fetched"
case (.inspect, .infinitive): "inspect"
case (.inspect, .progressive): "Inspecting"
case (.inspect, .past): "Inspected"
case (.list, .infinitive): "list"
case (.list, .progressive): "Listing"
case (.list, .past): "Listed"
case (.move, .infinitive): "move"
case (.move, .progressive): "Moving"
case (.move, .past): "Moved"
case (.present, .infinitive): "present"
case (.present, .progressive): "Writing"
case (.present, .past): "Presented"
case (.pull, .infinitive): "pull"
case (.pull, .progressive): "Pulling"
case (.pull, .past): "Pulled"
case (.push, .infinitive): "push"
case (.push, .progressive): "Pushing"
case (.push, .past): "Pushed"
case (.read, .infinitive): "read"
case (.read, .progressive): "Reading"
case (.read, .past): "Read"
case (.request, .infinitive): "request"
case (.request, .progressive): "Requesting"
case (.request, .past): "Requested"
case (.run, .infinitive): "run"
case (.run, .progressive): "Running"
case (.run, .past): "Ran"
case (.search, .infinitive): "search"
case (.search, .progressive): "Searching"
case (.search, .past): "Searched"
case (.suggest, .infinitive): "suggest"
case (.suggest, .progressive): "Suggesting"
case (.suggest, .past): "Suggested"
case (.update, .infinitive): "update"
case (.update, .progressive): "Updating"
case (.update, .past): "Updated"
case (.wait, .infinitive): "wait for"
case (.wait, .progressive): "Waiting for"
case (.wait, .past): "Waited for"
case (.write, .infinitive): "write"
case (.write, .progressive): "Writing"
case (.write, .past): "Wrote"
case (.changePermissions, .infinitive): "change permissions for"
case (.changePermissions, .progressive): "Changing permissions for"
case (.changePermissions, .past): "Changed permissions for"
case (.use, .infinitive): "use"
case (.use, .progressive): "Using"
case (.use, .past): "Used"
}
}
private static func localizedActionVerb(
_ kind: AgentToolDescriptor.Presentation.ActionKind, locale: Locale
) -> String {
switch kind {
case .ask: localized("ask", locale: locale)
case .compare: localized("compare", locale: locale)
case .create: localized("create", locale: locale)
case .delete: localized("delete", locale: locale)
case .edit: localized("edit", locale: locale)
case .fetch: localized("fetch", locale: locale)
case .inspect: localized("inspect", locale: locale)
case .list: localized("list", locale: locale)
case .move: localized("move", locale: locale)
case .present: localized("present", locale: locale)
case .pull: localized("pull", locale: locale)
case .push: localized("push", locale: locale)
case .read: localized("read", locale: locale)
case .request: localized("request", locale: locale)
case .run: localized("run", locale: locale)
case .search: localized("search", locale: locale)
case .suggest: localized("suggest", locale: locale)
case .update: localized("update", locale: locale)
case .wait: localized("wait for", locale: locale)
case .write: localized("write", locale: locale)
case .changePermissions: localized("change permissions for", locale: locale)
case .use: localized("use", locale: locale)
}
}
private static func isChinese(_ locale: Locale) -> Bool {
locale.language.languageCode?.identifier == "zh"
}
private static func activityName(
_ kind: AgentToolDescriptor.Presentation.ActivityKind, locale: Locale
) -> String {
switch kind {
case .command: localized("a command", locale: locale)
case .hosts: localized("hosts", locale: locale)
case .plan: localized("a plan", locale: locale)
case .question: localized("a question", locale: locale)
case .remoteCommand: localized("the remote command", locale: locale)
case .remoteFile: localized("a remote file", locale: locale)
case .scratchEntry: localized("a scratch entry", locale: locale)
case .scratchFile: localized("a scratch file", locale: locale)
case .scratchFiles: localized("scratch files", locale: locale)
case .scratchWorkspace: localized("the scratch workspace", locale: locale)
case .secret: localized("a secret", locale: locale)
case .sshCommand: localized("the SSH command", locale: locale)
case .taskList: localized("the task list", locale: locale)
case .url: localized("a URL", locale: locale)
case .web: localized("the web", locale: locale)
}
}
private static func localized(
_ key: String.LocalizationValue, locale: Locale
) -> String {
AppLocalization.string(key, locale: locale)
}
private static func legacyActivity(
_ fallback: String, descriptorName: String, locale: Locale
) -> String {
let sftpNames = [
"sftp_list", "sftp_stat", "sftp_read", "sftp_write", "sftp_mkdir",
"sftp_move", "sftp_chmod", "sftp_delete",
]
if sftpNames.contains(descriptorName) {
return activityName(.remoteFile, locale: locale)
}
let kind: AgentToolDescriptor.Presentation.ActivityKind? = switch (
descriptorName, fallback
) {
case ("fetch", "a URL"): .url
case ("web_search", "the web"): .web
case ("list_hosts", "hosts"): .hosts
case ("ssh_wait", "the remote command"): .remoteCommand
case ("ssh_exec", "the SSH command"): .sshCommand
case ("terminal_suggest", "a command"): .command
case ("request_user_input", "a question"): .question
case ("request_user_secret", "a secret"): .secret
case ("present_plan", "a plan"): .plan
case ("manage_tasks", "the task list"): .taskList
case ("scratch_list", "the scratch workspace"): .scratchWorkspace
case ("scratch_read", "a scratch file"), ("scratch_write", "a scratch file"),
("scratch_edit", "a scratch file"): .scratchFile
case ("scratch_diff", "scratch files"): .scratchFiles
case ("scratch_delete", "a scratch entry"): .scratchEntry
case ("scratch_pull", "a remote file"), ("scratch_push", "a remote file"):
.remoteFile
default: nil
}
return kind.map { activityName($0, locale: locale) } ?? fallback
}
private static func lowercaseFirst(_ text: String) -> String {
guard let first = text.first else { return text }
return first.lowercased() + String(text.dropFirst())
+15 -9
View File
@@ -366,32 +366,38 @@ nonisolated enum AgentWorkPresentation {
/// A compact, stable duration for the work disclosure. Seconds are floored
/// so a completed run never appears to gain time after its finish event.
static func duration(from startedAt: Date, to finishedAt: Date) -> String {
static func duration(
from startedAt: Date, to finishedAt: Date, locale: Locale = .current
) -> String {
let seconds = max(0, Int(finishedAt.timeIntervalSince(startedAt)))
let hours = seconds / 3_600
let minutes = (seconds % 3_600) / 60
let remainingSeconds = seconds % 60
if hours > 0 {
return minutes > 0 ? "\(hours)h \(minutes)m" : "\(hours)h"
return minutes > 0
? AppLocalization.string("\(hours)h \(minutes)m", locale: locale)
: AppLocalization.string("\(hours)h", locale: locale)
}
if minutes > 0 {
return remainingSeconds > 0
? "\(minutes)m \(remainingSeconds)s" : "\(minutes)m"
? AppLocalization.string("\(minutes)m \(remainingSeconds)s", locale: locale)
: AppLocalization.string("\(minutes)m", locale: locale)
}
return "\(remainingSeconds)s"
return AppLocalization.string("\(remainingSeconds)s", locale: locale)
}
static func title(
for state: AgentRunState, from startedAt: Date, to finishedAt: Date
for state: AgentRunState, from startedAt: Date, to finishedAt: Date,
locale: Locale = .current
) -> String {
let elapsed = duration(from: startedAt, to: finishedAt)
let elapsed = duration(from: startedAt, to: finishedAt, locale: locale)
switch state {
case .idle, .running, .waitingForUser, .waitingForApproval, .reviewing:
return "Working for \(elapsed)"
return AppLocalization.string("Working for \(elapsed)", locale: locale)
case .cancelled:
return "You stopped after \(elapsed)"
return AppLocalization.string("You stopped after \(elapsed)", locale: locale)
case .completed, .failed, .interrupted:
return "Worked for \(elapsed)"
return AppLocalization.string("Worked for \(elapsed)", locale: locale)
}
}
}
@@ -362,6 +362,7 @@ private struct AgentWorkGroupView: View {
let onExpansion: () -> Void
@State private var isExpanded: Bool
@Environment(\.locale) private var locale
init(group: AgentWorkGroup, isCompact: Bool, onExpansion: @escaping () -> Void) {
self.group = group
@@ -426,11 +427,17 @@ private struct AgentWorkGroupView: View {
.contentShape(.rect)
}
.buttonStyle(.plain)
.help(Text(isExpanded ? "Hide work" : "Show work"))
.help(Text(
isExpanded ? String(localized: "Hide work") : String(localized: "Show work")
))
.accessibilityLabel(Text(title(at: group.run.finishedAt ?? .now)))
.accessibilityValue(Text(isExpanded ? "Expanded" : "Collapsed"))
.accessibilityValue(Text(
isExpanded ? String(localized: "Expanded") : String(localized: "Collapsed")
))
.accessibilityHint(Text(
isExpanded ? "Collapse work process" : "Expand work process"
isExpanded
? String(localized: "Collapse work process")
: String(localized: "Expand work process")
))
if isExpanded { workItems }
@@ -487,7 +494,7 @@ private struct AgentWorkGroupView: View {
private func title(at now: Date) -> String {
let end = group.run.finishedAt ?? now
return AgentWorkPresentation.title(
for: group.run.state, from: group.run.startedAt, to: end
for: group.run.state, from: group.run.startedAt, to: end, locale: locale
)
}
}
@@ -500,6 +507,7 @@ private struct AgentToolBatchView: View {
let onExpansion: () -> Void
@Environment(AppModel.self) private var app
@Environment(\.locale) private var locale
@State private var isExpanded = false
private var cards: [AgentToolCard] { batch.cards }
@@ -522,11 +530,13 @@ private struct AgentToolBatchView: View {
private var title: String {
guard !unfinished.isEmpty else {
return AgentToolPresentation.batchSummary(for: cards)
return AgentToolPresentation.batchSummary(for: cards, locale: locale)
}
if let displayedReasoning { return displayedReasoning }
return unfinished.map { card in
AgentToolPresentation.summary(for: card, hostName: hostName(for: card))
AgentToolPresentation.summary(
for: card, hostName: hostName(for: card), locale: locale
)
}.joined(separator: "; ")
}
@@ -556,9 +566,13 @@ private struct AgentToolBatchView: View {
.contentShape(.rect)
}
.buttonStyle(.plain)
.help(Text(isExpanded ? "Hide tools" : "Show tools"))
.help(Text(
isExpanded ? String(localized: "Hide tools") : String(localized: "Show tools")
))
.accessibilityLabel(Text(title))
.accessibilityValue(Text(isExpanded ? "Expanded" : "Collapsed"))
.accessibilityValue(Text(
isExpanded ? String(localized: "Expanded") : String(localized: "Collapsed")
))
if isExpanded {
LazyVStack(alignment: .leading, spacing: Metrics.Chat.blockSpacing) {
@@ -600,10 +614,11 @@ private struct AgentToolRowView: View {
let onExpansion: () -> Void
@Environment(AppModel.self) private var app
@Environment(\.locale) private var locale
@State private var isExpanded = false
private var summary: String {
AgentToolPresentation.summary(for: card, hostName: hostName)
AgentToolPresentation.summary(for: card, hostName: hostName, locale: locale)
}
var body: some View {
@@ -641,7 +656,10 @@ private struct AgentToolRowView: View {
.contentShape(.rect)
}
.buttonStyle(.plain)
.help(Text(isExpanded ? "Hide details" : "Show details"))
.help(Text(
isExpanded
? String(localized: "Hide details") : String(localized: "Show details")
))
if isExpanded { detail }
}
@@ -651,7 +669,7 @@ private struct AgentToolRowView: View {
VStack(alignment: .leading, spacing: Metrics.Space.s) {
if let review = card.review {
Label(
"Security Review: \(review.risk.rawValue.capitalized) — \(review.reason)",
"Security Review: \(review.risk.displayName) — \(review.reason)",
systemImage: "checkmark.shield"
)
.font(.caption)
@@ -882,6 +900,18 @@ private struct AgentCompactionActivityRowView: View {
return reason
}
private var statusTitle: String {
failure == nil
? String(localized: "Compacting context")
: String(localized: "Couldn’t compact context")
}
private var helpTitle: String {
failure == nil
? String(localized: "Compacting context")
: String(localized: "Show failure details")
}
var body: some View {
VStack(alignment: .leading, spacing: Metrics.Space.s) {
Button {
@@ -900,7 +930,7 @@ private struct AgentCompactionActivityRowView: View {
.frame(width: Metrics.Sidebar.rowIcon)
}
Text(failure == nil ? "Compacting context" : "Couldn’t compact context")
Text(statusTitle)
.font(MarkdownTypography.body)
.foregroundStyle(failure == nil ? .secondary : .primary)
.modifier(ScanningTextMask(isActive: failure == nil))
@@ -916,7 +946,7 @@ private struct AgentCompactionActivityRowView: View {
.contentShape(.rect)
}
.buttonStyle(.plain)
.help(Text(failure == nil ? "Compacting context" : "Show failure details"))
.help(Text(helpTitle))
if isExpanded, let failure {
HStack(alignment: .top, spacing: Metrics.Space.m) {
@@ -1246,7 +1276,10 @@ private struct AgentPlanRowView: View {
.contentShape(.rect)
}
.buttonStyle(.plain)
.help(Text(isExpanded ? "Hide the plan" : "Show the plan"))
.help(Text(
isExpanded
? String(localized: "Hide the plan") : String(localized: "Show the plan")
))
}
private var actions: some View {
+4 -1
View File
@@ -144,7 +144,10 @@ private struct ChipView: View {
styledContent(removable: false)
}
}
.help(Text(remove == nil ? "Always included" : "Remove attachment"))
.help(Text(
remove == nil
? String(localized: "Always included") : String(localized: "Remove attachment")
))
}
private func styledContent(removable: Bool) -> some View {
@@ -141,7 +141,7 @@ private struct ShellCodeBlockView: View {
CopyButton(text: command)
}
.controlSize(.small)
.help(Text(target == nil ? "No shell in the foreground" : ""))
.help(Text(target == nil ? String(localized: "No shell in the foreground") : ""))
}
private func run() {
@@ -408,7 +408,10 @@ private struct AssistantConversationRow: View {
.font(.caption)
}
.buttonStyle(.plain)
.help(Text(conversation.isArchived ? "Unarchive" : "Archive"))
.help(Text(
conversation.isArchived
? String(localized: "Unarchive") : String(localized: "Archive")
))
}
}
}
+194 -110
View File
@@ -16,7 +16,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : " 复制"
"value" : "复制"
}
}
}
@@ -26,7 +26,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "[… %lld字符省略...]"
"value" : "[…%lld字符省略…]"
}
}
}
@@ -42,7 +42,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "%1$@ — %2$@"
"value" : "%1$@—%2$@"
}
}
}
@@ -73,7 +73,7 @@
"other" : {
"stringUnit" : {
"state" : "translated",
"value" : "%1$@ — %2$lld个工具"
"value" : "%1$@—%2$lld个工具"
}
}
}
@@ -87,7 +87,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "%@ — 1个工具"
"value" : "%@—1个工具"
}
}
}
@@ -283,7 +283,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "%1$@, %2$@"
"value" : "%1$@、%2$@"
}
}
}
@@ -299,7 +299,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "%1$@%2$@ / %3$@(%4$lld%%)"
"value" : "%1$@%2$@/%3$@(%4$lld%%)"
}
}
}
@@ -535,7 +535,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "已完成%1$lld / %2$lld个任务"
"value" : "已完成%1$lld/%2$lld个任务"
}
}
}
@@ -659,7 +659,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "%1$lld任务 ·%2$lld已完成"
"value" : "%1$lld个任务·已完成%2$lld个"
}
}
}
@@ -674,6 +674,16 @@
}
}
},
"%lld times" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld次"
}
}
}
},
"%lld tool calls" : {
"localizations" : {
"en" : {
@@ -743,7 +753,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "`%@` 可以不可逆转地破坏数据或状态。"
"value" : "`%@`可以不可逆地破坏数据或状态。"
}
}
}
@@ -753,7 +763,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "`%@` 更改系统或文件状态。"
"value" : "`%@`会更改系统或文件状态。"
}
}
}
@@ -763,7 +773,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "`%@` 不是一个只读的命令。"
"value" : "`%@`不是可识别的只读命令。"
}
}
}
@@ -773,7 +783,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "`crontab -r` 会直接清空crontab,不作确认。"
"value" : "`crontab -r`会直接清空crontab且不作确认。"
}
}
}
@@ -783,7 +793,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "`docker system prune` 广泛回收和删除。"
"value" : "`docker system prune`会大范围回收并删除数据。"
}
}
}
@@ -793,7 +803,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "`find -delete` 删除每个匹配。"
"value" : "`find -delete`会删除每个匹配项。"
}
}
}
@@ -803,7 +813,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "`find -exec` 运行每个匹配的命令。"
"value" : "`find -exec`会为每个匹配项运行命令。"
}
}
}
@@ -813,7 +823,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "`git clean` 删除未跟踪的文件。"
"value" : "`git clean`会删除未跟踪的文件。"
}
}
}
@@ -823,7 +833,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "`git reset --hard` 丢弃未承诺的工作。"
"value" : "`git reset --hard`会丢弃未提交的更改。"
}
}
}
@@ -833,7 +843,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "`rsync --delete` 删除来源中缺失的文件。"
"value" : "`rsync --delete`会删除源中不存在的文件。"
}
}
}
@@ -843,7 +853,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "`sed -i` 编辑已设置的文件。"
"value" : "`sed -i`会直接修改文件。"
}
}
}
@@ -935,7 +945,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "1 / %lld"
"value" : "1/%lld"
}
}
}
@@ -1836,7 +1846,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "Bearer Token"
"value" : "Bearer令牌"
}
}
}
@@ -1886,7 +1896,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "将输入广播到此工作区的每个SSH会话 — ⌘B"
"value" : "将输入广播到此工作区的每个SSH会话—⌘B"
}
}
}
@@ -1896,7 +1906,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "使用SFTP浏览 ·%@"
"value" : "使用SFTP浏览·%@"
}
}
}
@@ -2236,7 +2246,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "清除此主机的命令历史 ?"
"value" : "清除此主机的命令历史?"
}
}
}
@@ -2296,7 +2306,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "点击可插入 ^ 双击可执行"
"value" : "单击插入·双击执行"
}
}
}
@@ -2326,7 +2336,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "关闭查找—— Esc"
"value" : "关闭查找—Esc"
}
}
}
@@ -2386,7 +2396,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "关闭标签 - 单击以关闭其他标签"
"value" : "关闭标签—按住⌥单击可关闭其他标签"
}
}
}
@@ -2416,7 +2426,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "折叠工作流程"
"value" : "收起工作过程"
}
}
}
@@ -2582,7 +2592,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "紧凑—— %1$@汇总为%2$@"
"value" : "已压缩:%1$@汇总为%2$@"
}
}
}
@@ -2592,7 +2602,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "紧凑的背景"
"value" : "正在压缩上下文"
}
}
}
@@ -2722,7 +2732,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "使用SSH连接 ·%@"
"value" : "使用SSH连接·%@"
}
}
}
@@ -3182,7 +3192,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "创建"
"value" : "已创建"
}
}
}
@@ -3342,7 +3352,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "删除%lld物品吗?"
"value" : "删除%lld项吗?"
}
}
}
@@ -3362,7 +3372,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "删除对话吗 ?"
"value" : "删除对话吗?"
}
}
}
@@ -3532,7 +3542,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "删除"
"value" : "已删除"
}
}
}
@@ -3832,7 +3842,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "动态代理 (-D)"
"value" : "动态代理(-D)"
}
}
}
@@ -4122,7 +4132,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "执行破坏命令?"
"value" : "执行破坏性命令?"
}
}
}
@@ -4132,7 +4142,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "执行毁灭剧本?"
"value" : "执行破坏性脚本?"
}
}
}
@@ -4142,7 +4152,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "执行破坏性剧本?"
"value" : "执行破坏性脚本?"
}
}
}
@@ -4212,7 +4222,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "扩大工作进程"
"value" : "展开工作过程"
}
}
}
@@ -4278,7 +4288,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "导出%lld 1主机的密钥"
"value" : "已将%lld个密钥导出到1台主机"
}
}
}
@@ -4348,7 +4358,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "提取器会写文件,也许会覆盖"
"value" : "提取操作会写入文件,并可能覆盖现有内容。"
}
}
}
@@ -4848,7 +4858,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "Google拒绝登录- %@"
"value" : "Google拒绝登录:%@"
}
}
}
@@ -4929,7 +4939,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "隐藏细节"
"value" : "收起详情"
}
}
}
@@ -4969,7 +4979,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "隐藏工具"
"value" : "收起工具"
}
}
}
@@ -4979,7 +4989,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "隐藏工作"
"value" : "收起工作过程"
}
}
}
@@ -5069,7 +5079,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "主机 : %@"
"value" : "主机:%@"
}
}
}
@@ -5451,7 +5461,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "跳板机%1$lld (%2$@): %3$@"
"value" : "跳板机%1$lld(%2$@):%3$@"
}
}
}
@@ -5531,7 +5541,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "密钥生成失败 : %@"
"value" : "密钥生成失败:%@"
}
}
}
@@ -6037,7 +6047,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "模型%1$@,努力时%2$@"
"value" : "模型%1$@,推理强度%2$@"
}
}
}
@@ -6327,7 +6337,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "下一个匹配项 — ⇧Return、⌘G、F3"
"value" : "下一个匹配项—⇧Return、⌘G、F3"
}
}
}
@@ -6357,7 +6367,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "NIST P-256信息技术科"
"value" : "NIST P-256曲线"
}
}
}
@@ -6457,7 +6467,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "没有命令在那个句柄下运行。 报告完成后已经报告了,或者在经过长时间的询问后被释放。"
"value" : "该句柄下没有正在运行的命令。命令可能已结束并报告结果,或因长时间无人查询而被释放。"
}
}
}
@@ -6707,7 +6717,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "没有可辨认的言论被俘获. 靠近麦克风说话"
"value" : "未捕获到可识别的语音。请靠近麦克风后重试。"
}
}
}
@@ -6907,7 +6917,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "非基数64 PEM"
"value" : "不是Base64编码的PEM"
}
}
}
@@ -7027,7 +7037,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "在助手的模型选择器和 /model命令中提供此模型。"
"value" : "在助手模型选择器和/model命令中提供此模型。"
}
}
}
@@ -7237,7 +7247,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "覆盖 `%@`。"
"value" : "覆盖`%@`。"
}
}
}
@@ -7497,7 +7507,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "权限 : %@"
"value" : "权限:%@"
}
}
}
@@ -7717,7 +7727,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "上一个匹配项 — Return、⇧⌘G、⇧F3"
"value" : "上一个匹配项—Return、⇧⌘G、⇧F3"
}
}
}
@@ -7953,7 +7963,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "问题%1$lld / %2$lld"
"value" : "问题%1$lld/%2$lld"
}
}
}
@@ -7973,7 +7983,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "加入当前代理的队列 — ⏎"
"value" : "加入当前代理的队列—⏎"
}
}
}
@@ -8053,7 +8063,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "到达了后端,但没有结果。"
"value" : "已连接后端,但未返回结果。"
}
}
}
@@ -8159,7 +8169,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "第%1$lld次重连 ·%2$lld秒后重试"
"value" : "第%1$lld次重连·%2$lld秒后重试"
}
}
}
@@ -8199,7 +8209,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "正在重新连接... 尝试%lld"
"value" : "正在重新连接…第%lld次尝试"
}
}
}
@@ -8485,7 +8495,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "替换此私钥会改变其公钥. 使用旧密钥的主机可能拒绝下一个连接。"
"value" : "替换此私钥会改变其公钥。使用旧密钥的主机可能拒绝下次连接。"
}
}
}
@@ -8875,7 +8885,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "运行... %lld连接"
"value" : "正在运行·%lld个连接"
}
}
}
@@ -8885,7 +8895,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "运行中... 1个连接"
"value" : "正在运行·1个连接"
}
}
}
@@ -9115,7 +9125,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "无法配置安全飞地访问 : %@"
"value" : "无法配置安全隔区访问:%@"
}
}
}
@@ -9135,7 +9145,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "安全飞地密钥无法移动到另一个存储位置。"
"value" : "安全隔区密钥无法移动到其他存储位置。"
}
}
}
@@ -9145,7 +9155,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "安全飞地密钥无法存储SSH证书。"
"value" : "安全隔区密钥无法存储SSH证书。"
}
}
}
@@ -9231,7 +9241,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "安全审查: %1$@ — %2$@"
"value" : "安全审查:%1$@—%2$@"
}
}
}
@@ -9291,7 +9301,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "选择至少两个主机。 包含一个主机的工作区是一个标签。"
"value" : "请至少选择两台主机。仅含一台主机时应使用标签页。"
}
}
}
@@ -9351,7 +9361,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "发送 — ⌘⏎"
"value" : "发送—⌘⏎"
}
}
}
@@ -9381,7 +9391,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "在授权信头中发送。 下面的自定义头作为纯文本存储。"
"value" : "通过Authorization标头发送。下方的自定义标头以纯文本存储。"
}
}
}
@@ -9541,7 +9551,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "显示细节"
"value" : "展开详情"
}
}
}
@@ -9551,7 +9561,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "显示失败细节"
"value" : "展开失败详情"
}
}
}
@@ -9631,7 +9641,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "显示工具"
"value" : "展开工具"
}
}
}
@@ -9641,7 +9651,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "显示工作"
"value" : "展开工作过程"
}
}
}
@@ -9771,7 +9781,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "这台Mac上的SOCKS5 → SSH主机 → 请求目标"
"value" : "这台Mac上的SOCKS5→SSH主机→请求目标"
}
}
}
@@ -9871,7 +9881,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "SSH连接失败 : %@我该检查什么?"
"value" : "SSH连接失败:%@。我该检查什么?"
}
}
}
@@ -9891,7 +9901,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "SSH主机 → 这台Mac → 目标"
"value" : "SSH主机→这台Mac→目标"
}
}
}
@@ -9927,7 +9937,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "标准输出 :\n%1$@\n标准错误 :\n%2$@"
"value" : "标准输出:\n%1$@\n标准错误:\n%2$@"
}
}
}
@@ -10027,7 +10037,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "追加指示"
"value" : "追加"
}
}
}
@@ -10043,7 +10053,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "步骤%1$lld / %2$lld"
"value" : "步骤%1$lld/%2$lld"
}
}
}
@@ -10379,7 +10389,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "任务,已完成%1$lld / %2$lld"
"value" : "任务,已完成%1$lld/%2$lld"
}
}
}
@@ -10449,7 +10459,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "Termind无法启动音频输入。 检查是否有麦克风连接和可用。"
"value" : "Termind无法启动音频输入。请检查麦克风是否已连接且可用。"
}
}
}
@@ -10555,7 +10565,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "文本相同,但换行符不同:%1$@ → %2$@"
"value" : "文本相同,但换行符不同:%1$@→%2$@"
}
}
}
@@ -10635,7 +10645,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "对话太大,无法概括。"
"value" : "对话过长,无法生成摘要。"
}
}
}
@@ -10701,7 +10711,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "不同的区域范围%1$lld线条,通过%2$lld- 线diff限制。 用每一边的定界读数来缩小它"
"value" : "差异区域跨越%1$lld行,超过%2$lld行的差异限制。请分别读取两侧的有限范围以缩小差异。"
}
}
}
@@ -10791,7 +10801,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "文件大于25 MB。"
"value" : "文件大于25MB。"
}
}
}
@@ -11017,7 +11027,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "钥匙串 项并不包含预期的凭据类型。"
"value" : "钥匙串项目不包含预期的凭据类型。"
}
}
}
@@ -11163,7 +11173,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "处理后的图像大于5 MB。"
"value" : "处理后的图像大于5MB。"
}
}
}
@@ -11193,7 +11203,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "私钥不能离开安全飞地 只有它的名字可以改变。"
"value" : "私钥无法离开安全隔区,只能更改其名称。"
}
}
}
@@ -11203,7 +11213,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "无法读取私钥 : %@"
"value" : "无法读取私钥:%@"
}
}
}
@@ -11313,7 +11323,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "此Mac上没有保存的密码。 在更改存储前再次输入。"
"value" : "这台Mac上无法使用已保存的密码。更改存储方式前请重新输入。"
}
}
}
@@ -11413,7 +11423,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "选中的模型无法读取图像。 发送前选择可视化模型。"
"value" : "所选模型无法读取图像。发送前请选择支持视觉的模型。"
}
}
}
@@ -11463,7 +11473,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "服务器仅监听127.0.0.1和 ::1,默认关闭。启用后,外部调用无需逐次批准即可执行。"
"value" : "服务器仅监听127.0.0.1和::1,默认关闭。启用后,外部调用无需逐次批准即可执行。"
}
}
}
@@ -11771,7 +11781,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "这个Mac SSH主机 目标"
"value" : "这台Mac→SSH主机→目标"
}
}
}
@@ -11811,7 +11821,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "此PDF没有可选择的文本。 不支持扫描的PDF。"
"value" : "此PDF没有可选择的文本,不支持扫描版PDF。"
}
}
}
@@ -11921,7 +11931,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "工具输入被拒绝 : %@"
"value" : "工具输入被拒绝:%@"
}
}
}
@@ -12191,7 +12201,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "未知或已禁用的工具 : %@"
"value" : "未知或已禁用的工具:%@"
}
}
}
@@ -12221,7 +12231,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "未知工具 : %@"
"value" : "未知工具:%@"
}
}
}
@@ -12301,7 +12311,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "不支持的SSH证书算法 : %@"
"value" : "不支持的SSH证书算法:%@"
}
}
}
@@ -12311,7 +12321,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "不支持的SSH密钥算法 : %@"
"value" : "不支持的SSH密钥算法:%@"
}
}
}
@@ -12341,7 +12351,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "已是最新 · %@"
"value" : "已是最新·%@"
}
}
}
@@ -12431,7 +12441,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "使用Vertex AI软件"
"value" : "使用Vertex AI"
}
}
}
@@ -12441,7 +12451,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "使用您的安全飞地SSH密钥"
"value" : "使用你的安全隔区SSH密钥"
}
}
}
@@ -12741,7 +12751,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "怎样强调?"
"value" : "需要强调的内容"
}
}
}
@@ -12886,6 +12896,80 @@
}
}
},
"%lldh %lldm" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"%1$lld小时%2$lld分钟"}}}},
"%lldh" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"%lld小时"}}}},
"%lldm %llds" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"%1$lld分钟%2$lld秒"}}}},
"%lldm" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"%lld分钟"}}}},
"%llds" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"%lld秒"}}}},
"Working for %@" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"已工作%@"}}}},
"Worked for %@" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"工作了%@"}}}},
"You stopped after %@" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"你在%@后停止了"}}}},
"ask" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"询问"}}}},
"compare" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"比较"}}}},
"create" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"创建"}}}},
"delete" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"删除"}}}},
"edit" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"编辑"}}}},
"fetch" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"获取"}}}},
"inspect" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"检查"}}}},
"list" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"列出"}}}},
"move" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"移动"}}}},
"present" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"提交"}}}},
"pull" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"拉取"}}}},
"push" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"推送"}}}},
"read" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"读取"}}}},
"request" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"请求"}}}},
"run" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"运行"}}}},
"search" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"搜索"}}}},
"suggest" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"生成建议"}}}},
"update" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"更新"}}}},
"wait for" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"等待"}}}},
"write" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"写入"}}}},
"change permissions for" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"更改权限"}}}},
"use" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"使用"}}}},
"a command" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"命令"}}}},
"hosts" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"主机"}}}},
"a plan" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"计划"}}}},
"a question" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"问题"}}}},
"the remote command" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"远程命令"}}}},
"a remote file" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"远程文件"}}}},
"a scratch entry" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"暂存条目"}}}},
"a scratch file" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"暂存文件"}}}},
"scratch files" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"暂存文件"}}}},
"the scratch workspace" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"暂存工作区"}}}},
"a secret" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"密钥信息"}}}},
"the SSH command" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"SSH命令"}}}},
"a URL" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"URL"}}}},
"the web" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"网络"}}}},
"Repeated tool action failed: %lld %@" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"%1$lld次%2$@失败"}}}},
"Repeated tool action interrupted: %lld %@" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"%1$lld次%2$@已中断"}}}},
"Tool action preparing: %@ %@" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"正在准备%1$@%2$@"}}}},
"Tool action waiting: %@ %@" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"等待%1$@%2$@"}}}},
"Tool action running: %@ %@" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"正在%1$@%2$@"}}}},
"Tool action completed: %@ %@" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"已%1$@%2$@"}}}},
"Tool action denied: %@ %@" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"未%1$@%2$@"}}}},
"Tool action failed: %@ %@" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"%1$@%2$@失败"}}}},
"Tool action interrupted: %@ %@" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"%1$@%2$@已中断"}}}},
"Tool action target: %@ %@" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"%1$@(%2$@)"}}}},
"Preparing to run “%@” on %@" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"正在准备在%2$@上运行“%1$@”"}}}},
"Preparing to run “%@”" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"正在准备运行“%@”"}}}},
"Waiting to run “%@” on %@" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"等待在%2$@上运行“%1$@”"}}}},
"Waiting to run “%@”" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"等待运行“%@”"}}}},
"Waiting to authenticate with %@" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"等待向%@进行身份验证"}}}},
"Connecting to %@" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"正在连接%@"}}}},
"Running “%@” on %@" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"正在%2$@上运行“%1$@”"}}}},
"Running “%@”" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"正在运行“%@”"}}}},
"Ran “%@” on %@" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"已在%2$@上运行“%1$@”"}}}},
"Ran “%@”" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"已运行“%@”"}}}},
"Did not run “%@” on %@" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"未在%2$@上运行“%1$@”"}}}},
"Did not run “%@”" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"未运行“%@”"}}}},
"Failed to run “%@” on %@" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"未能在%2$@上运行“%1$@”"}}}},
"Failed to run “%@”" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"未能运行“%@”"}}}},
"Stopped waiting for “%@” on %@" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"已停止等待%2$@上的“%1$@”"}}}},
"Stopped waiting for “%@”" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"已停止等待“%@”"}}}},
"File" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"文件"}}}},
"Directory" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"目录"}}}},
"Symlink" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"符号链接"}}}},
"Critical" : {"localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"严重"}}}},
"You said" : {
"localizations" : {
"zh-Hans" : {
@@ -12898,4 +12982,4 @@
}
},
"version" : "1.2"
}
}
@@ -142,6 +142,144 @@ struct AgentToolPresentationTests {
)
}
@Test
func semanticActionsLocalizeEveryLifecycleWithoutChangingArguments() {
let zh = Locale(identifier: "zh-Hans")
let actions: [(AgentToolDescriptor.Presentation.ActionKind, String)] = [
(.ask, "询问"), (.compare, "比较"), (.create, "创建"), (.delete, "删除"),
(.edit, "编辑"), (.fetch, "获取"), (.inspect, "检查"), (.list, "列出"),
(.move, "移动"), (.present, "提交"), (.pull, "拉取"), (.push, "推送"),
(.read, "读取"), (.request, "请求"), (.run, "运行"), (.search, "搜索"),
(.suggest, "生成建议"), (.update, "更新"), (.wait, "等待"), (.write, "写入"),
(.changePermissions, "更改权限"), (.use, "使用"),
]
for (kind, verb) in actions {
var card = makeCard(
name: "semantic_\(kind.rawValue)", target: .local,
arguments: ["value": .string("Target 42")],
presentation: .init(
symbol: "wrench", activity: .semanticArgument(key: "value", fallback: .command),
actionKind: kind
)
)
let expected: [(AgentToolCard.State, String)] = [
(.proposed, "正在准备\(verb)“Target 42”"),
(.waitingForApproval, "等待\(verb)“Target 42”"),
(.running, "正在\(verb)“Target 42”"),
(.completed, "已\(verb)“Target 42”"),
(.denied, "未\(verb)“Target 42”"),
(.failed, "\(verb)“Target 42”失败"),
(.interrupted, "\(verb)“Target 42”已中断"),
]
for (state, title) in expected {
card.state = state
#expect(AgentToolPresentation.summary(for: card, locale: zh) == title)
}
}
}
@Test
func localizedSearchBatchSSHAndMCPKeepUserAndRemoteTextVerbatim() {
let zh = Locale(identifier: "zh-Hans")
var search = makeCard(
name: "web_search", target: .network,
arguments: ["query": .string("Swift 6 concurrency")],
presentation: .init(
symbol: "magnifyingglass",
activity: .semanticArgument(key: "query", fallback: .web),
actionKind: .search
)
)
search.state = .completed
#expect(
AgentToolPresentation.summary(for: search, locale: zh)
== "已搜索“Swift 6 concurrency”"
)
var fetch = makeCard(
name: "fetch", target: .network,
presentation: .init(
symbol: "globe", activity: .semanticArgument(key: "url", fallback: .url),
actionKind: .fetch
)
)
fetch.state = .failed
#expect(
AgentToolPresentation.batchSummary(for: [search, fetch], locale: zh)
== "已搜索“Swift 6 concurrency”,获取URL失败"
)
var ssh = makeCard(
name: "ssh_exec", target: .host,
arguments: ["command": .string("printf 'hello world'")],
presentation: AgentBuiltInTools.terminalSuggest().descriptor.presentation
)
ssh.state = .completed
#expect(
AgentToolPresentation.summary(for: ssh, hostName: "production-1", locale: zh)
== "已在production-1上运行“printf 'hello world'”"
)
var mcp = makeCard(
name: "mcp__airtable__find_records", target: .mcp,
presentation: .init(
symbol: "puzzlepiece.extension", activity: .label("airtable.find_records"),
actionKind: .run
)
)
mcp.state = .running
#expect(
AgentToolPresentation.summary(for: mcp, locale: zh)
== "正在运行airtable.find_records"
)
mcp.state = .failed
#expect(
AgentToolPresentation.batchSummary(for: [mcp, mcp], locale: zh)
== "2次运行失败"
)
mcp.state = .interrupted
#expect(
AgentToolPresentation.batchSummary(for: [mcp, mcp], locale: zh)
== "2次运行已中断"
)
}
@Test
func appOwnedActivitiesAreUnquotedAndLegacySearchUsesItsQuery() {
let zh = Locale(identifier: "zh-Hans")
var hosts = makeCard(name: "list_hosts", target: .host)
hosts.state = .completed
#expect(AgentToolPresentation.summary(for: hosts, locale: zh) == "已列出主机")
var restoredSearch = makeCard(
name: "web_search", target: .network,
arguments: ["query": .string("Swift concurrency")]
)
restoredSearch.state = .completed
#expect(
AgentToolPresentation.summary(for: restoredSearch, locale: zh)
== "已搜索“Swift concurrency”"
)
var restoredSearchWithoutQuery = makeCard(name: "web_search", target: .network)
restoredSearchWithoutQuery.state = .completed
#expect(
AgentToolPresentation.summary(for: restoredSearchWithoutQuery, locale: zh)
== "已搜索"
)
#expect(
AgentToolPresentation.batchSummary(for: [restoredSearchWithoutQuery], locale: zh)
== "已搜索"
)
#expect(
AgentToolPresentation.batchSummary(
for: [restoredSearchWithoutQuery, restoredSearchWithoutQuery], locale: zh
) == "已搜索2次"
)
}
@Test
func userInputSummaryCountsQuestionsAndUsesAskGrammar() {
let descriptor = AgentBuiltInTools.requestUserInput().descriptor
@@ -704,6 +842,63 @@ struct AgentToolPresentationTests {
#expect(result.metadata[AgentToolResult.toolPresentationKey] != nil)
}
@Test
func semanticPresentationRoundTripsAndOldMetadataUsesSafeDefaults() throws {
let semantic = AgentToolDescriptor.Presentation(
symbol: "magnifyingglass",
activity: .semanticArgument(key: "query", fallback: .web),
output: .json, groupsWithAdjacentTools: false, actionKind: .search,
phaseKinds: ["running": .sshConnecting]
)
let encoded = try JSONEncoder().encode(semantic)
#expect(
try JSONDecoder().decode(AgentToolDescriptor.Presentation.self, from: encoded)
== semantic
)
let legacy = AgentToolDescriptor.Presentation(
symbol: "terminal", activity: .argument(key: "command", fallback: "a command")
)
var object = try #require(
JSONSerialization.jsonObject(with: JSONEncoder().encode(legacy)) as? [String: Any]
)
object["groupsWithAdjacentTools"] = nil
object["actionKind"] = nil
object["phaseKinds"] = nil
let decoded = try JSONDecoder().decode(
AgentToolDescriptor.Presentation.self,
from: JSONSerialization.data(withJSONObject: object)
)
#expect(decoded.groupsWithAdjacentTools)
#expect(decoded.actionKind == nil)
#expect(decoded.phaseKinds.isEmpty)
}
@Test
func legacyCountedArrayActivityStillDecodesAndLocalizesKnownQuestions() throws {
let activity = AgentToolDescriptor.Presentation.Activity.arrayCount(
key: "questions", singular: "a question", plural: "questions",
fallback: "a question"
)
let data = try JSONEncoder().encode(activity)
#expect(try JSONDecoder().decode(type(of: activity), from: data) == activity)
var card = makeCard(
name: "request_user_input", target: .user,
arguments: ["questions": .array([.object([:]), .object([:])])],
presentation: .init(
symbol: "questionmark.circle", activity: activity,
action: .init(infinitive: "ask", progressive: "Asking", past: "Asked")
)
)
card.state = .completed
#expect(
AgentToolPresentation.summary(
for: card, locale: Locale(identifier: "zh-Hans")
) == "已询问2个问题"
)
}
@Test
func builtInToolsKeepDistinctStableIcons() {
#expect(
+1 -1
View File
@@ -99,7 +99,7 @@ struct AppLanguageTests {
)
let strings = try #require(root["strings"] as? [String: Any])
let expected = [
"App": "App",
"App": "应用",
"Version": "版本",
"Welcome": "欢迎",
"Snippets": "片段",
+34 -7
View File
@@ -1136,7 +1136,8 @@ struct ChatComposerStateTests {
let run = conversation.workGroups.first?.run
#expect(run.map {
AgentWorkPresentation.title(
for: $0.state, from: $0.startedAt, to: $0.finishedAt ?? start
for: $0.state, from: $0.startedAt, to: $0.finishedAt ?? start,
locale: Locale(identifier: "en")
)
} == "You stopped after 1m 2s")
}
@@ -1196,9 +1197,17 @@ struct ChatComposerStateTests {
@Test
func workDurationUsesAtMostTwoUnitsAndSuccessfulRunsCollapseByDefault() {
let start = Date(timeIntervalSince1970: 1_000)
#expect(AgentWorkPresentation.duration(from: start, to: start.addingTimeInterval(42)) == "42s")
#expect(AgentWorkPresentation.duration(from: start, to: start.addingTimeInterval(68)) == "1m 8s")
#expect(AgentWorkPresentation.duration(from: start, to: start.addingTimeInterval(4_320)) == "1h 12m")
let en = Locale(identifier: "en")
let zh = Locale(identifier: "zh-Hans")
#expect(AgentWorkPresentation.duration(
from: start, to: start.addingTimeInterval(42), locale: en
) == "42s")
#expect(AgentWorkPresentation.duration(
from: start, to: start.addingTimeInterval(68), locale: en
) == "1m 8s")
#expect(AgentWorkPresentation.duration(
from: start, to: start.addingTimeInterval(4_320), locale: en
) == "1h 12m")
#expect(!AgentWorkPresentation.isExpandedByDefault(for: .completed))
#expect(AgentWorkPresentation.isExpandedByDefault(for: .running))
#expect(AgentWorkPresentation.isExpandedByDefault(for: .waitingForUser))
@@ -1206,14 +1215,32 @@ struct ChatComposerStateTests {
#expect(AgentWorkPresentation.isExpandedByDefault(for: .cancelled))
#expect(AgentWorkPresentation.isExpandedByDefault(for: .interrupted))
#expect(AgentWorkPresentation.title(
for: .running, from: start, to: start.addingTimeInterval(68)
for: .running, from: start, to: start.addingTimeInterval(68), locale: en
) == "Working for 1m 8s")
#expect(AgentWorkPresentation.title(
for: .completed, from: start, to: start.addingTimeInterval(68)
for: .completed, from: start, to: start.addingTimeInterval(68), locale: en
) == "Worked for 1m 8s")
#expect(AgentWorkPresentation.title(
for: .cancelled, from: start, to: start.addingTimeInterval(62)
for: .cancelled, from: start, to: start.addingTimeInterval(62), locale: en
) == "You stopped after 1m 2s")
#expect(AgentWorkPresentation.duration(
from: start, to: start.addingTimeInterval(42), locale: zh
) == "42秒")
#expect(AgentWorkPresentation.duration(
from: start, to: start.addingTimeInterval(68), locale: zh
) == "1分钟8秒")
#expect(AgentWorkPresentation.duration(
from: start, to: start.addingTimeInterval(4_320), locale: zh
) == "1小时12分钟")
#expect(AgentWorkPresentation.title(
for: .running, from: start, to: start.addingTimeInterval(68), locale: zh
) == "已工作1分钟8秒")
#expect(AgentWorkPresentation.title(
for: .completed, from: start, to: start.addingTimeInterval(68), locale: zh
) == "工作了1分钟8秒")
#expect(AgentWorkPresentation.title(
for: .cancelled, from: start, to: start.addingTimeInterval(62), locale: zh
) == "你在1分钟2秒后停止了")
}
/// A turn that only called tools has nothing to say, and an empty bubble