Improve Simplified Chinese tool presentation and localization
This commit is contained in:
1 parent
af0dd561e2
commit
c8229ff645
22 files changed
+1435
-261
No files matched your search
Executable
+63
@@ -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."
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -99,7 +99,7 @@ struct AppLanguageTests {
|
||||
)
|
||||
let strings = try #require(root["strings"] as? [String: Any])
|
||||
let expected = [
|
||||
"App": "App",
|
||||
"App": "应用",
|
||||
"Version": "版本",
|
||||
"Welcome": "欢迎",
|
||||
"Snippets": "片段",
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user