Russian UI: settings, notch views, agent steps, errors; visible success after installing hooks

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
maksarsanjeev
2026-10-06 20:15:30 +03:00
parent 750e5ad0ad
commit 319cdc8b18
20 changed files with 565 additions and 519 deletions
+56 -56
View File
@@ -329,7 +329,7 @@ final class HookServer: @unchecked Sendable {
?? "unknown"
let cwd = payload["cwd"] as? String ?? ""
let rawName = URL(fileURLWithPath: cwd).lastPathComponent
let projectName = aliasProjectName(rawName.isEmpty ? "Session" : rawName)
let projectName = aliasProjectName(rawName.isEmpty ? "Сессия" : rawName)
// Determine which pill this event belongs to.
// coucou_agent must be lowercase, digits and hyphens, ≤ 24 chars.
@@ -387,9 +387,9 @@ final class HookServer: @unchecked Sendable {
if let pending = state.pendingApproval, agentId == pending.pillId {
let handledNote: String
switch pending.pillId {
case "agent_cursor": handledNote = "Handled in Cursor."
case "agent_codex": handledNote = "Handled in Codex."
default: handledNote = "Handled in VS Code."
case "agent_cursor": handledNote = "Решено в Cursor."
case "agent_codex": handledNote = "Решено в Codex."
default: handledNote = "Решено в VS Code."
}
var resolved = false
switch name {
@@ -461,7 +461,7 @@ final class HookServer: @unchecked Sendable {
case "PostToolUseFailure":
state.updateTask(id: agentId, state: .working)
appendStep(id: agentId, step: "⚠ failed")
appendStep(id: agentId, step: "⚠ ошибка")
case "Notification":
let message = payload["message"] as? String ?? ""
@@ -522,10 +522,10 @@ final class HookServer: @unchecked Sendable {
state.removeTask(id: agentId)
case "SubagentStart":
appendStep(id: agentId, step: "+ subagent")
appendStep(id: agentId, step: "+ субагент")
case "SubagentStop":
appendStep(id: agentId, step: "• subagent done")
appendStep(id: agentId, step: "• субагент готов")
default:
break
@@ -613,7 +613,7 @@ final class HookServer: @unchecked Sendable {
?? "unknown"
let cwd = payload["cwd"] as? String ?? ""
let rawName = URL(fileURLWithPath: cwd).lastPathComponent
let projectName = aliasProjectName(rawName.isEmpty ? "Session" : rawName)
let projectName = aliasProjectName(rawName.isEmpty ? "Сессия" : rawName)
let rawAgent = payload["coucou_agent"] as? String ?? ""
let termProgram = payload["term_program"] as? String ?? ""
@@ -710,9 +710,9 @@ final class HookServer: @unchecked Sendable {
guard let self, self.pendingApprovalFD == fd else { return }
let note: String
switch capturedPillId {
case "agent_cursor": note = "Handled in Cursor."
case "agent_codex": note = "Handled in Codex."
default: note = "Handled in VS Code."
case "agent_cursor": note = "Решено в Cursor."
case "agent_codex": note = "Решено в Codex."
default: note = "Решено в VS Code."
}
self.dismissApprovalCard(note: note)
}
@@ -727,9 +727,9 @@ final class HookServer: @unchecked Sendable {
guard let self, self.pendingApprovalFD == captured else { return }
let note: String
switch capturedPillId {
case "agent_cursor": note = "Still waiting in Cursor."
case "agent_codex": note = "Still waiting in Codex."
default: note = "Still waiting in VS Code."
case "agent_cursor": note = "Всё ещё ждёт в Cursor."
case "agent_codex": note = "Всё ещё ждёт в Codex."
default: note = "Всё ещё ждёт в VS Code."
}
self.dismissApprovalCard(note: note)
}
@@ -789,7 +789,7 @@ final class HookServer: @unchecked Sendable {
?? "unknown"
let cwd = payload["cwd"] as? String ?? ""
let rawName = URL(fileURLWithPath: cwd).lastPathComponent
let projectName = aliasProjectName(rawName.isEmpty ? "Session" : rawName)
let projectName = aliasProjectName(rawName.isEmpty ? "Сессия" : rawName)
let rawAgent = payload["coucou_agent"] as? String ?? ""
let termProgram = payload["term_program"] as? String ?? ""
@@ -930,27 +930,27 @@ final class HookServer: @unchecked Sendable {
return aliases[name.lowercased()] ?? name
}
// MARK: - French step labels
// MARK: - Step labels (Russian)
private func frenchStep(tool: String, input: [String: Any]) -> String {
let labels: [String: String] = [
"Bash": "Exécute",
"Read": "Lit",
"Write": "Écrit",
"Edit": "Modifie",
"Glob": "Cherche",
"Grep": "Recherche",
"WebSearch": "Recherche web",
"WebFetch": "Récupère",
"TodoWrite": "Tâches",
"Task": "Agent",
"LS": "Liste",
"MultiEdit": "Modifie",
"NotebookEdit": "Notebook",
"Bash": "Выполняет",
"Read": "Читает",
"Write": "Пишет",
"Edit": "Правит",
"Glob": "Ищет",
"Grep": "Ищет",
"WebSearch": "Веб-поиск",
"WebFetch": "Загружает",
"TodoWrite": "Задачи",
"Task": "Агент",
"LS": "Список",
"MultiEdit": "Правит",
"NotebookEdit": "Блокнот",
// Codex tools
"apply_patch": "Modifie",
"update_plan": "Tâches",
"spawn_agent": "Agent",
"apply_patch": "Правит",
"update_plan": "Задачи",
"spawn_agent": "Агент",
]
var label = labels[tool] ?? tool
@@ -991,19 +991,19 @@ final class HookServer: @unchecked Sendable {
return label
}
/// Infers a French verb from a shell command's first word.
/// Infers a Russian verb from a shell command's first word.
private func bashVerb(_ command: String) -> String {
let first = command.split(whereSeparator: { $0.isWhitespace }).first.map(String.init) ?? ""
switch first {
case "cat", "bat", "head", "tail", "less", "more", "nl": return "Lit"
case "rg", "grep", "find", "fd", "ls", "tree", "wc": return "Cherche"
case "cat", "bat", "head", "tail", "less", "more", "nl": return "Читает"
case "rg", "grep", "find", "fd", "ls", "tree", "wc": return "Ищет"
default: break
}
let testRunners = ["pytest", "vitest", "jest", "npm test", "npm run test",
"cargo test", "go test", "swift test", "make test",
"xcodebuild test", "unittest"]
if testRunners.contains(where: { command.contains($0) }) { return "Teste" }
return "Exécute"
if testRunners.contains(where: { command.contains($0) }) { return "Тестирует" }
return "Выполняет"
}
// MARK: - Live diff helpers
@@ -1284,7 +1284,7 @@ final class HookServer: @unchecked Sendable {
if clCheck.terminationStatus != 0 {
throw NSError(domain: "Coucou", code: 1,
userInfo: [NSLocalizedDescriptionKey:
"Command Line Tools are required but not installed. Run: xcode-select --install"])
"Нужны Command Line Tools, но они не установлены. Выполните: xcode-select --install"])
}
if let existing = oldSL,
@@ -1332,13 +1332,13 @@ final class HookServer: @unchecked Sendable {
// Build a compact diff: show only the statusLine key before → after
func slJSON(_ val: [String: Any]?) throws -> String {
guard let v = val else { return "(none)" }
guard let v = val else { return "(нет)" }
let d = try JSONSerialization.data(withJSONObject: v, options: [.prettyPrinted, .sortedKeys])
return String(data: d, encoding: .utf8) ?? "(none)"
return String(data: d, encoding: .utf8) ?? "(нет)"
}
let before = try slJSON(oldSL)
let after = try slJSON(newSL)
return "statusLine\nBefore:\n\(before)\n\nAfter:\n\(after)"
return "statusLine\nБыло:\n\(before)\n\nСтало:\n\(after)"
}
/// Writes settings.json and commits side effects (call after user confirms).
@@ -1511,7 +1511,7 @@ final class HookServer: @unchecked Sendable {
let exists = FileManager.default.fileExists(atPath: url.path)
if !install && !exists {
throw NSError(domain: "CoucouNoop", code: 0, userInfo: [
NSLocalizedDescriptionKey: "No Gemini CLI hooks to remove."
NSLocalizedDescriptionKey: "Нет хуков Gemini CLI для удаления."
])
}
let current = exists ? try Data(contentsOf: url) : Data()
@@ -1527,7 +1527,7 @@ final class HookServer: @unchecked Sendable {
let current = (try? Data(contentsOf: url)) ?? Data()
guard sha256Hex(current) == fp else {
throw NSError(domain: "Coucou", code: 1, userInfo: [
NSLocalizedDescriptionKey: "~/.gemini/settings.json changed since preview. Refresh and try again."
NSLocalizedDescriptionKey: "~/.gemini/settings.json изменился после предпросмотра. Обновите и попробуйте снова."
])
}
try writeJSONFile(data, to: url, suffix: "settings.json")
@@ -1540,7 +1540,7 @@ final class HookServer: @unchecked Sendable {
label: "~/.gemini/settings.json")
if let raw = settings["hooks"], !(raw is [String: Any]) {
throw NSError(domain: "Coucou", code: 2, userInfo: [
NSLocalizedDescriptionKey: "~/.gemini/settings.json: \"hooks\" has an unexpected type — BroV has not touched it."
NSLocalizedDescriptionKey: "~/.gemini/settings.json: \"hooks\" имеет неожиданный тип — BroV его не трогал."
])
}
let base = hookBase()
@@ -1557,7 +1557,7 @@ final class HookServer: @unchecked Sendable {
for (geminiEvent, normalizedEvent, timeout) in events {
if let raw = hooks[geminiEvent], !(raw is [[String: Any]]) {
throw NSError(domain: "Coucou", code: 2, userInfo: [
NSLocalizedDescriptionKey: "~/.gemini/settings.json: \"hooks\"[\"\(geminiEvent)\"] has an unexpected type — BroV has not touched it."
NSLocalizedDescriptionKey: "~/.gemini/settings.json: \"hooks\"[\"\(geminiEvent)\"] имеет неожиданный тип — BroV его не трогал."
])
}
var groups = hooks[geminiEvent] as? [[String: Any]] ?? []
@@ -1581,7 +1581,7 @@ final class HookServer: @unchecked Sendable {
label: "~/.gemini/settings.json")
if let raw = settings["hooks"], !(raw is [String: Any]) {
throw NSError(domain: "Coucou", code: 2, userInfo: [
NSLocalizedDescriptionKey: "~/.gemini/settings.json: \"hooks\" has an unexpected type — BroV has not touched it."
NSLocalizedDescriptionKey: "~/.gemini/settings.json: \"hooks\" имеет неожиданный тип — BroV его не трогал."
])
}
if var hooks = settings["hooks"] as? [String: Any] {
@@ -1607,7 +1607,7 @@ final class HookServer: @unchecked Sendable {
let exists = FileManager.default.fileExists(atPath: url.path)
if !install && !exists {
throw NSError(domain: "CoucouNoop", code: 0, userInfo: [
NSLocalizedDescriptionKey: "No Antigravity hooks to remove."
NSLocalizedDescriptionKey: "Нет хуков Antigravity для удаления."
])
}
let current = exists ? try Data(contentsOf: url) : Data()
@@ -1623,7 +1623,7 @@ final class HookServer: @unchecked Sendable {
let current = (try? Data(contentsOf: url)) ?? Data()
guard sha256Hex(current) == fp else {
throw NSError(domain: "Coucou", code: 1, userInfo: [
NSLocalizedDescriptionKey: "~/.gemini/config/hooks.json changed since preview. Refresh and try again."
NSLocalizedDescriptionKey: "~/.gemini/config/hooks.json изменился после предпросмотра. Обновите и попробуйте снова."
])
}
try writeJSONFile(data, to: url, suffix: "hooks.json")
@@ -1679,12 +1679,12 @@ final class HookServer: @unchecked Sendable {
do { data = try Data(contentsOf: url) }
catch {
throw NSError(domain: "Coucou", code: 2, userInfo: [
NSLocalizedDescriptionKey: "\(label) cannot be read — BroV has not touched it."
NSLocalizedDescriptionKey: "Не удалось прочитать \(label) — BroV его не трогал."
])
}
guard let obj = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any] else {
throw NSError(domain: "Coucou", code: 2, userInfo: [
NSLocalizedDescriptionKey: "\(label) is not valid JSON — BroV has not touched it."
NSLocalizedDescriptionKey: "\(label) — некорректный JSON, BroV его не трогал."
])
}
return obj
@@ -1702,7 +1702,7 @@ final class HookServer: @unchecked Sendable {
do { try fm.copyItem(at: url, to: backupURL) }
catch {
throw NSError(domain: "Coucou", code: 3, userInfo: [
NSLocalizedDescriptionKey: "Could not back up \(url.lastPathComponent): \(error.localizedDescription)"
NSLocalizedDescriptionKey: "Не удалось сделать резервную копию \(url.lastPathComponent): \(error.localizedDescription)"
])
}
}
@@ -1762,7 +1762,7 @@ final class HookServer: @unchecked Sendable {
let exists = FileManager.default.fileExists(atPath: url.path)
if !install && !exists {
throw NSError(domain: "CoucouNoop", code: 0, userInfo: [
NSLocalizedDescriptionKey: "No Codex hooks to remove."
NSLocalizedDescriptionKey: "Нет хуков Codex для удаления."
])
}
let current = exists ? try Data(contentsOf: url) : Data()
@@ -1778,7 +1778,7 @@ final class HookServer: @unchecked Sendable {
let current = (try? Data(contentsOf: url)) ?? Data()
guard sha256Hex(current) == fp else {
throw NSError(domain: "Coucou", code: 1, userInfo: [
NSLocalizedDescriptionKey: "~/.codex/hooks.json changed since preview. Refresh and try again."
NSLocalizedDescriptionKey: "~/.codex/hooks.json изменился после предпросмотра. Обновите и попробуйте снова."
])
}
try writeJSONFile(data, to: url, suffix: "hooks.json")
@@ -1790,7 +1790,7 @@ final class HookServer: @unchecked Sendable {
var root = try Self.strictReadJSONObject(at: Self.codexHooksURL, label: "~/.codex/hooks.json")
if let raw = root["hooks"], !(raw is [String: Any]) {
throw NSError(domain: "Coucou", code: 2, userInfo: [
NSLocalizedDescriptionKey: "~/.codex/hooks.json: \"hooks\" has an unexpected type — BroV has not touched it."
NSLocalizedDescriptionKey: "~/.codex/hooks.json: \"hooks\" имеет неожиданный тип — BroV его не трогал."
])
}
let base = hookBase()
@@ -1812,7 +1812,7 @@ final class HookServer: @unchecked Sendable {
for (event, timeout, statusMsg) in events {
if let raw = hooks[event], !(raw is [[String: Any]]) {
throw NSError(domain: "Coucou", code: 2, userInfo: [
NSLocalizedDescriptionKey: "~/.codex/hooks.json: \"hooks\"[\"\(event)\"] has an unexpected type — BroV has not touched it."
NSLocalizedDescriptionKey: "~/.codex/hooks.json: \"hooks\"[\"\(event)\"] имеет неожиданный тип — BroV его не трогал."
])
}
var groups = hooks[event] as? [[String: Any]] ?? []
@@ -1836,7 +1836,7 @@ final class HookServer: @unchecked Sendable {
var root = try Self.strictReadJSONObject(at: Self.codexHooksURL, label: "~/.codex/hooks.json")
if let raw = root["hooks"], !(raw is [String: Any]) {
throw NSError(domain: "Coucou", code: 2, userInfo: [
NSLocalizedDescriptionKey: "~/.codex/hooks.json: \"hooks\" has an unexpected type — BroV has not touched it."
NSLocalizedDescriptionKey: "~/.codex/hooks.json: \"hooks\" имеет неожиданный тип — BroV его не трогал."
])
}
if var hooks = root["hooks"] as? [String: Any] {