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:
@@ -34,11 +34,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
button.image?.isTemplate = true
|
button.image?.isTemplate = true
|
||||||
|
|
||||||
let menu = NSMenu()
|
let menu = NSMenu()
|
||||||
menu.addItem(withTitle: "Open BroV", action: #selector(openIsland), keyEquivalent: "")
|
menu.addItem(withTitle: "Открыть BroV", action: #selector(openIsland), keyEquivalent: "")
|
||||||
menu.addItem(.separator())
|
menu.addItem(.separator())
|
||||||
menu.addItem(withTitle: "Settings…", action: #selector(openSettings), keyEquivalent: ",")
|
menu.addItem(withTitle: "Настройки…", action: #selector(openSettings), keyEquivalent: ",")
|
||||||
menu.addItem(.separator())
|
menu.addItem(.separator())
|
||||||
menu.addItem(withTitle: "Quit", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")
|
menu.addItem(withTitle: "Выйти", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")
|
||||||
|
|
||||||
statusItem?.menu = menu
|
statusItem?.menu = menu
|
||||||
}
|
}
|
||||||
@@ -69,7 +69,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
let win = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 720, height: 560),
|
let win = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 720, height: 560),
|
||||||
styleMask: [.titled, .closable, .miniaturizable, .resizable],
|
styleMask: [.titled, .closable, .miniaturizable, .resizable],
|
||||||
backing: .buffered, defer: false)
|
backing: .buffered, defer: false)
|
||||||
win.title = "Settings — BroV"
|
win.title = "Настройки — BroV"
|
||||||
let host = NSHostingView(rootView: SettingsView())
|
let host = NSHostingView(rootView: SettingsView())
|
||||||
host.sizingOptions = [.minSize]
|
host.sizingOptions = [.minSize]
|
||||||
win.contentView = host
|
win.contentView = host
|
||||||
|
|||||||
@@ -150,8 +150,8 @@ final class AppState: ObservableObject {
|
|||||||
let normalised = LocalChat.normaliseURL(baseURL)
|
let normalised = LocalChat.normaliseURL(baseURL)
|
||||||
guard !normalised.isEmpty else {
|
guard !normalised.isEmpty else {
|
||||||
providerModelFetchError[provider] = provider == .ollama
|
providerModelFetchError[provider] = provider == .ollama
|
||||||
? "Connect Ollama in Settings → Chat first."
|
? "Сначала подключите Ollama в Настройки → Чат."
|
||||||
: "Connect LM Studio in Settings → Chat first."
|
: "Сначала подключите LM Studio в Настройки → Чат."
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
loadingProviderModels.insert(provider)
|
loadingProviderModels.insert(provider)
|
||||||
@@ -162,8 +162,8 @@ final class AppState: ObservableObject {
|
|||||||
switch result {
|
switch result {
|
||||||
case .success(let models) where models.isEmpty:
|
case .success(let models) where models.isEmpty:
|
||||||
providerModelFetchError[provider] = provider == .ollama
|
providerModelFetchError[provider] = provider == .ollama
|
||||||
? "No models yet. Download one in Ollama first."
|
? "Моделей пока нет. Скачайте модель в Ollama."
|
||||||
: "No models yet. Download one in LM Studio first."
|
: "Моделей пока нет. Скачайте модель в LM Studio."
|
||||||
case .success(let models):
|
case .success(let models):
|
||||||
fetchedProviderModels[provider] = models
|
fetchedProviderModels[provider] = models
|
||||||
let current = provider == .ollama ? ollamaChatModel : lmstudioChatModel
|
let current = provider == .ollama ? ollamaChatModel : lmstudioChatModel
|
||||||
@@ -173,14 +173,14 @@ final class AppState: ObservableObject {
|
|||||||
else { lmstudioChatModel = first }
|
else { lmstudioChatModel = first }
|
||||||
}
|
}
|
||||||
case .failure:
|
case .failure:
|
||||||
providerModelFetchError[provider] = "Cannot reach \(normalised). Is the server running?"
|
providerModelFetchError[provider] = "Нет связи с \(normalised). Сервер запущен?"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Remote providers: require API key
|
// Remote providers: require API key
|
||||||
guard let apiKey = KeychainStore.shared.get(provider.keychainKey), !apiKey.isEmpty else {
|
guard let apiKey = KeychainStore.shared.get(provider.keychainKey), !apiKey.isEmpty else {
|
||||||
providerModelFetchError[provider] = "No API key — add it in Settings."
|
providerModelFetchError[provider] = "Нет API-ключа — добавьте его в Настройки → Чат."
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
loadingProviderModels.insert(provider)
|
loadingProviderModels.insert(provider)
|
||||||
@@ -196,7 +196,7 @@ final class AppState: ObservableObject {
|
|||||||
}
|
}
|
||||||
loadingProviderModels.remove(provider)
|
loadingProviderModels.remove(provider)
|
||||||
if models.isEmpty {
|
if models.isEmpty {
|
||||||
providerModelFetchError[provider] = "Failed to load models. Check your API key."
|
providerModelFetchError[provider] = "Не удалось загрузить модели. Проверьте API-ключ."
|
||||||
} else {
|
} else {
|
||||||
fetchedProviderModels[provider] = models
|
fetchedProviderModels[provider] = models
|
||||||
switch provider {
|
switch provider {
|
||||||
@@ -680,13 +680,13 @@ struct VercelDeployment: Identifiable {
|
|||||||
let branch: String?
|
let branch: String?
|
||||||
|
|
||||||
var isSuccess: Bool { state == "READY" }
|
var isSuccess: Bool { state == "READY" }
|
||||||
var statusLabel: String { isSuccess ? "Ready" : (state == "CANCELED" ? "Canceled" : "Error") }
|
var statusLabel: String { isSuccess ? "Готово" : (state == "CANCELED" ? "Отменено" : "Ошибка") }
|
||||||
var timeAgo: String {
|
var timeAgo: String {
|
||||||
let diff = Date().timeIntervalSince(createdAt)
|
let diff = Date().timeIntervalSince(createdAt)
|
||||||
if diff < 60 { return "just now" }
|
if diff < 60 { return "только что" }
|
||||||
if diff < 3600 { return "\(Int(diff/60))m" }
|
if diff < 3600 { return "\(Int(diff/60)) мин" }
|
||||||
if diff < 86400 { return "\(Int(diff/3600))h" }
|
if diff < 86400 { return "\(Int(diff/3600)) ч" }
|
||||||
return "\(Int(diff/86400))d"
|
return "\(Int(diff/86400)) д"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -705,10 +705,10 @@ struct ResendEmail: Identifiable {
|
|||||||
}
|
}
|
||||||
var timeAgo: String {
|
var timeAgo: String {
|
||||||
let diff = Date().timeIntervalSince(createdAt)
|
let diff = Date().timeIntervalSince(createdAt)
|
||||||
if diff < 60 { return "just now" }
|
if diff < 60 { return "только что" }
|
||||||
if diff < 3600 { return "\(Int(diff/60))m" }
|
if diff < 3600 { return "\(Int(diff/60)) мин" }
|
||||||
if diff < 86400 { return "\(Int(diff/3600))h" }
|
if diff < 86400 { return "\(Int(diff/3600)) ч" }
|
||||||
return "\(Int(diff/86400))d"
|
return "\(Int(diff/86400)) д"
|
||||||
}
|
}
|
||||||
var isDelivered: Bool { lastEvent == "delivered" }
|
var isDelivered: Bool { lastEvent == "delivered" }
|
||||||
}
|
}
|
||||||
@@ -734,10 +734,10 @@ struct StripePayment: Identifiable, Equatable {
|
|||||||
var isSuccess: Bool { status == "succeeded" }
|
var isSuccess: Bool { status == "succeeded" }
|
||||||
var timeAgo: String {
|
var timeAgo: String {
|
||||||
let diff = Date().timeIntervalSince(createdAt)
|
let diff = Date().timeIntervalSince(createdAt)
|
||||||
if diff < 60 { return "just now" }
|
if diff < 60 { return "только что" }
|
||||||
if diff < 3600 { return "\(Int(diff/60))m" }
|
if diff < 3600 { return "\(Int(diff/60)) мин" }
|
||||||
if diff < 86400 { return "\(Int(diff/3600))h" }
|
if diff < 86400 { return "\(Int(diff/3600)) ч" }
|
||||||
return "\(Int(diff/86400))d"
|
return "\(Int(diff/86400)) д"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -781,10 +781,10 @@ struct NotionPage: Identifiable {
|
|||||||
|
|
||||||
var timeAgo: String {
|
var timeAgo: String {
|
||||||
let diff = Date().timeIntervalSince(lastEditedAt)
|
let diff = Date().timeIntervalSince(lastEditedAt)
|
||||||
if diff < 60 { return "now" }
|
if diff < 60 { return "сейчас" }
|
||||||
if diff < 3600 { return "\(Int(diff/60))m" }
|
if diff < 3600 { return "\(Int(diff/60)) мин" }
|
||||||
if diff < 86400 { return "\(Int(diff/3600))h" }
|
if diff < 86400 { return "\(Int(diff/3600)) ч" }
|
||||||
return "\(Int(diff/86400))d"
|
return "\(Int(diff/86400)) д"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,9 +42,9 @@ final class CalcomPoller: @unchecked Sendable {
|
|||||||
let code = (response as? HTTPURLResponse)?.statusCode ?? 0
|
let code = (response as? HTTPURLResponse)?.statusCode ?? 0
|
||||||
if code != 200 {
|
if code != 200 {
|
||||||
let msg: String
|
let msg: String
|
||||||
if code == 401 { msg = "Invalid API key (401)" }
|
if code == 401 { msg = "Неверный API-ключ (401)" }
|
||||||
else if code == 0 { msg = error?.localizedDescription ?? "No connection" }
|
else if code == 0 { msg = error?.localizedDescription ?? "Нет соединения" }
|
||||||
else { msg = "API error \(code)" }
|
else { msg = "Ошибка API \(code)" }
|
||||||
DispatchQueue.main.async { AppState.shared.calcomError = msg }
|
DispatchQueue.main.async { AppState.shared.calcomError = msg }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ enum ClaudeCodeCLIError: LocalizedError {
|
|||||||
var errorDescription: String? {
|
var errorDescription: String? {
|
||||||
switch self {
|
switch self {
|
||||||
case .notFound:
|
case .notFound:
|
||||||
"Claude Code CLI not found. Install it, or set its path in Settings → Chat."
|
"Claude Code CLI не найден. Установите его или укажите путь в Настройки → Чат."
|
||||||
case .failed(let message):
|
case .failed(let message):
|
||||||
message
|
message
|
||||||
}
|
}
|
||||||
@@ -31,7 +31,7 @@ final class ClaudeCodeCLI {
|
|||||||
|
|
||||||
/// Models offered in the picker. Empty id = whatever the CLI is configured to use.
|
/// Models offered in the picker. Empty id = whatever the CLI is configured to use.
|
||||||
static let models: [(id: String, label: String)] = [
|
static let models: [(id: String, label: String)] = [
|
||||||
("", "Default"),
|
("", "По умолчанию"),
|
||||||
("opus", "Opus"),
|
("opus", "Opus"),
|
||||||
("sonnet", "Sonnet"),
|
("sonnet", "Sonnet"),
|
||||||
("haiku", "Haiku"),
|
("haiku", "Haiku"),
|
||||||
@@ -149,7 +149,7 @@ final class ClaudeCodeCLI {
|
|||||||
|
|
||||||
if let error = parser.errorMessage {
|
if let error = parser.errorMessage {
|
||||||
if error.localizedCaseInsensitiveContains("authenticate") || error.localizedCaseInsensitiveContains("login") {
|
if error.localizedCaseInsensitiveContains("authenticate") || error.localizedCaseInsensitiveContains("login") {
|
||||||
throw ClaudeCodeCLIError.failed("Claude Code isn't logged in. Run `claude /login` in a terminal.")
|
throw ClaudeCodeCLIError.failed("Claude Code не авторизован. Выполните `claude /login` в терминале.")
|
||||||
}
|
}
|
||||||
// A resume that points at a gone session: start fresh next time.
|
// A resume that points at a gone session: start fresh next time.
|
||||||
if error.localizedCaseInsensitiveContains("No conversation found") { sessionID = nil }
|
if error.localizedCaseInsensitiveContains("No conversation found") { sessionID = nil }
|
||||||
@@ -158,7 +158,7 @@ final class ClaudeCodeCLI {
|
|||||||
if p.terminationReason == .uncaughtSignal { throw CancellationError() }
|
if p.terminationReason == .uncaughtSignal { throw CancellationError() }
|
||||||
if parser.finalText == nil, p.terminationStatus != 0 {
|
if parser.finalText == nil, p.terminationStatus != 0 {
|
||||||
let stderr = String(data: err.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? ""
|
let stderr = String(data: err.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? ""
|
||||||
let last = stderr.split(separator: "\n").last.map(String.init) ?? "exit code \(p.terminationStatus)"
|
let last = stderr.split(separator: "\n").last.map(String.init) ?? "код выхода \(p.terminationStatus)"
|
||||||
throw ClaudeCodeCLIError.failed("claude: \(last)")
|
throw ClaudeCodeCLIError.failed("claude: \(last)")
|
||||||
}
|
}
|
||||||
return parser.finalText ?? parser.visibleText
|
return parser.finalText ?? parser.visibleText
|
||||||
@@ -238,7 +238,7 @@ struct StreamParser {
|
|||||||
let isError = json["is_error"] as? Bool ?? false
|
let isError = json["is_error"] as? Bool ?? false
|
||||||
let result = json["result"] as? String
|
let result = json["result"] as? String
|
||||||
if isError {
|
if isError {
|
||||||
if errorMessage == nil { errorMessage = result ?? "Claude Code returned an error." }
|
if errorMessage == nil { errorMessage = result ?? "Claude Code вернул ошибку." }
|
||||||
} else {
|
} else {
|
||||||
// Keep the tool trail when there was one; otherwise the clean final answer.
|
// Keep the tool trail when there was one; otherwise the clean final answer.
|
||||||
finalText = committed.isEmpty ? (result ?? "") : committed
|
finalText = committed.isEmpty ? (result ?? "") : committed
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ struct ClaudePlanCardView: View {
|
|||||||
Circle()
|
Circle()
|
||||||
.fill(Color(hex: ClaudePlanGauge.color(for: dominant)))
|
.fill(Color(hex: ClaudePlanGauge.color(for: dominant)))
|
||||||
.frame(width: 7, height: 7)
|
.frame(width: 7, height: 7)
|
||||||
Text("Claude plan")
|
Text("План Claude")
|
||||||
.font(.system(size: 12, weight: .semibold))
|
.font(.system(size: 12, weight: .semibold))
|
||||||
.foregroundColor(Color(hex: "#F5F6F8"))
|
.foregroundColor(Color(hex: "#F5F6F8"))
|
||||||
Text(subtitleText)
|
Text(subtitleText)
|
||||||
@@ -30,8 +30,8 @@ struct ClaudePlanCardView: View {
|
|||||||
|
|
||||||
// Gauge rows
|
// Gauge rows
|
||||||
VStack(alignment: .leading, spacing: 5) {
|
VStack(alignment: .leading, spacing: 5) {
|
||||||
GaugeRowView(label: "5 hours", window: usage?.fiveHour, now: now)
|
GaugeRowView(label: "5 часов", window: usage?.fiveHour, now: now)
|
||||||
GaugeRowView(label: "Week", window: usage?.sevenDay, now: now, weekly: true)
|
GaugeRowView(label: "Неделя", window: usage?.sevenDay, now: now, weekly: true)
|
||||||
}
|
}
|
||||||
.padding(.top, 8)
|
.padding(.top, 8)
|
||||||
.padding(.leading, 108)
|
.padding(.leading, 108)
|
||||||
@@ -48,21 +48,21 @@ struct ClaudePlanCardView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var subtitleText: String {
|
private var subtitleText: String {
|
||||||
guard let usage else { return "Waiting for a Claude Code reply" }
|
guard let usage else { return "Жду ответа Claude Code" }
|
||||||
let diff = now.timeIntervalSince(usage.updatedAt)
|
let diff = now.timeIntervalSince(usage.updatedAt)
|
||||||
if diff < 60 { return "just now" }
|
if diff < 60 { return "только что" }
|
||||||
let mins = Int(diff / 60)
|
let mins = Int(diff / 60)
|
||||||
if mins < 60 { return "\(mins) min ago" }
|
if mins < 60 { return "\(mins) мин назад" }
|
||||||
return "\(mins / 60) h ago"
|
return "\(mins / 60) ч назад"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Gauge Row
|
// MARK: - Gauge Row
|
||||||
|
|
||||||
// DateFormatter created once, in English, for the weekly reset label
|
// DateFormatter created once, in Russian, for the weekly reset label
|
||||||
private let weeklyResetFormatter: DateFormatter = {
|
private let weeklyResetFormatter: DateFormatter = {
|
||||||
let fmt = DateFormatter()
|
let fmt = DateFormatter()
|
||||||
fmt.locale = Locale(identifier: "en_US_POSIX")
|
fmt.locale = Locale(identifier: "ru_RU")
|
||||||
fmt.dateFormat = "EEE H:mm"
|
fmt.dateFormat = "EEE H:mm"
|
||||||
return fmt
|
return fmt
|
||||||
}()
|
}()
|
||||||
@@ -78,7 +78,7 @@ private struct GaugeRowView: View {
|
|||||||
Text(label)
|
Text(label)
|
||||||
.font(.system(size: 11))
|
.font(.system(size: 11))
|
||||||
.foregroundColor(Color(hex: "#6B7079"))
|
.foregroundColor(Color(hex: "#6B7079"))
|
||||||
.frame(width: 40, alignment: .leading)
|
.frame(width: 46, alignment: .leading)
|
||||||
if let w = window {
|
if let w = window {
|
||||||
let pct = ClaudePlanGauge.effectivePct(w)
|
let pct = ClaudePlanGauge.effectivePct(w)
|
||||||
let accent = Color(hex: ClaudePlanGauge.color(for: pct))
|
let accent = Color(hex: ClaudePlanGauge.color(for: pct))
|
||||||
@@ -115,14 +115,14 @@ private struct GaugeRowView: View {
|
|||||||
|
|
||||||
private func resetLabel(_ w: PlanWindow) -> String {
|
private func resetLabel(_ w: PlanWindow) -> String {
|
||||||
let secs = w.resetsAt.timeIntervalSince(now)
|
let secs = w.resetsAt.timeIntervalSince(now)
|
||||||
guard secs > 0 else { return "Resetting…" }
|
guard secs > 0 else { return "Сброс…" }
|
||||||
if weekly {
|
if weekly {
|
||||||
return weeklyResetFormatter.string(from: w.resetsAt)
|
return weeklyResetFormatter.string(from: w.resetsAt)
|
||||||
} else {
|
} else {
|
||||||
let h = Int(secs / 3600)
|
let h = Int(secs / 3600)
|
||||||
let m = Int((secs.truncatingRemainder(dividingBy: 3600)) / 60)
|
let m = Int((secs.truncatingRemainder(dividingBy: 3600)) / 60)
|
||||||
if h > 0 { return "in \(h) h \(m)" }
|
if h > 0 { return "через \(h) ч \(m) мин" }
|
||||||
return "in \(m) min"
|
return "через \(m) мин"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ enum ClaudePlanGauge {
|
|||||||
|
|
||||||
/// Label shown in the island pill chip.
|
/// Label shown in the island pill chip.
|
||||||
static func pillLabel(_ usage: PlanUsage?) -> String {
|
static func pillLabel(_ usage: PlanUsage?) -> String {
|
||||||
guard let usage, let pct = dominantPct(usage) else { return "Claude plan" }
|
guard let usage, let pct = dominantPct(usage) else { return "План Claude" }
|
||||||
return "Claude \(Int(pct.rounded()))%"
|
return "Claude \(Int(pct.rounded()))%"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -226,7 +226,7 @@ final class ClaudeService {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let key = apiKey, !key.isEmpty else {
|
guard let key = apiKey, !key.isEmpty else {
|
||||||
await showError("API key missing. Open settings.", state: state)
|
await showError("Нет API-ключа. Откройте Настройки.", state: state)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,7 +350,7 @@ final class ClaudeService {
|
|||||||
guard !baseURL.isEmpty else {
|
guard !baseURL.isEmpty else {
|
||||||
if provider.isLocal {
|
if provider.isLocal {
|
||||||
let name = provider == .ollama ? "Ollama" : "LM Studio"
|
let name = provider == .ollama ? "Ollama" : "LM Studio"
|
||||||
await showError("Connect \(name) in Settings → Chat first.", state: state)
|
await showError("Сначала подключите \(name) в Настройки → Чат.", state: state)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -362,7 +362,7 @@ final class ClaudeService {
|
|||||||
authHeader = "Bearer ollama"
|
authHeader = "Bearer ollama"
|
||||||
} else {
|
} else {
|
||||||
guard let key = KeychainStore.shared.get(provider.keychainKey), !key.isEmpty else {
|
guard let key = KeychainStore.shared.get(provider.keychainKey), !key.isEmpty else {
|
||||||
await showError("\(provider.displayName) API key missing. Configure it in Settings.", state: state)
|
await showError("Нет API-ключа \(provider.displayName). Добавьте его в Настройки → Чат.", state: state)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
authHeader = "Bearer \(key)"
|
authHeader = "Bearer \(key)"
|
||||||
@@ -463,10 +463,10 @@ final class ClaudeService {
|
|||||||
switch e {
|
switch e {
|
||||||
case .serverUnreachable:
|
case .serverUnreachable:
|
||||||
msg = provider == .ollama
|
msg = provider == .ollama
|
||||||
? "Ollama isn't running. Open it, then ask again."
|
? "Ollama не запущена. Откройте её и спросите снова."
|
||||||
: "Start the local server in LM Studio, then ask again."
|
: "Запустите локальный сервер в LM Studio и спросите снова."
|
||||||
case .modelNotFound(let m):
|
case .modelNotFound(let m):
|
||||||
msg = "\(m) isn't installed. Pick another model above the chat box."
|
msg = "Модель \(m) не установлена. Выберите другую над полем чата."
|
||||||
case .serverError(let s):
|
case .serverError(let s):
|
||||||
msg = s
|
msg = s
|
||||||
}
|
}
|
||||||
@@ -492,7 +492,7 @@ final class ClaudeService {
|
|||||||
let choices = json["choices"] as? [[String: Any]],
|
let choices = json["choices"] as? [[String: Any]],
|
||||||
let message = choices.first?["message"] as? [String: Any],
|
let message = choices.first?["message"] as? [String: Any],
|
||||||
let content = message["content"] as? String else {
|
let content = message["content"] as? String else {
|
||||||
throw NSError(domain: "ChatAPI", code: 0, userInfo: [NSLocalizedDescriptionKey: "Unexpected response format"])
|
throw NSError(domain: "ChatAPI", code: 0, userInfo: [NSLocalizedDescriptionKey: "Неожиданный формат ответа"])
|
||||||
}
|
}
|
||||||
let trimmed = content.trimmingCharacters(in: .whitespacesAndNewlines)
|
let trimmed = content.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
conversationMessages.append(["role": "assistant", "content": trimmed])
|
conversationMessages.append(["role": "assistant", "content": trimmed])
|
||||||
@@ -511,7 +511,7 @@ final class ClaudeService {
|
|||||||
|
|
||||||
func search(query: String, context: PromptContext?, state: AppState) async {
|
func search(query: String, context: PromptContext?, state: AppState) async {
|
||||||
guard let key = apiKey, !key.isEmpty else {
|
guard let key = apiKey, !key.isEmpty else {
|
||||||
await showError("Anthropic API key missing. Open settings to configure it.", state: state)
|
await showError("Нет API-ключа Anthropic. Добавьте его в Настройки → Чат.", state: state)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -582,12 +582,12 @@ final class ClaudeService {
|
|||||||
let id = AppState.shared.claudeModel
|
let id = AppState.shared.claudeModel
|
||||||
throw NSError(domain: "Claude", code: 0,
|
throw NSError(domain: "Claude", code: 0,
|
||||||
userInfo: [NSLocalizedDescriptionKey:
|
userInfo: [NSLocalizedDescriptionKey:
|
||||||
"Model not found: \(id). Pick another one in Settings."])
|
"Модель не найдена: \(id). Выберите другую в Настройках."])
|
||||||
}
|
}
|
||||||
throw NSError(domain: "Claude", code: 0,
|
throw NSError(domain: "Claude", code: 0,
|
||||||
userInfo: [NSLocalizedDescriptionKey: errMsg])
|
userInfo: [NSLocalizedDescriptionKey: errMsg])
|
||||||
}
|
}
|
||||||
let msg = String(data: data, encoding: .utf8) ?? "unknown error"
|
let msg = String(data: data, encoding: .utf8) ?? "неизвестная ошибка"
|
||||||
throw NSError(domain: "Claude", code: 0, userInfo: [NSLocalizedDescriptionKey: msg])
|
throw NSError(domain: "Claude", code: 0, userInfo: [NSLocalizedDescriptionKey: msg])
|
||||||
}
|
}
|
||||||
return data
|
return data
|
||||||
@@ -598,7 +598,7 @@ final class ClaudeService {
|
|||||||
private func handleChatResult(_ data: Data, state: AppState) async {
|
private func handleChatResult(_ data: Data, state: AppState) async {
|
||||||
guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||||
let content = json["content"] as? [[String: Any]] else {
|
let content = json["content"] as? [[String: Any]] else {
|
||||||
await showError("Unexpected API response.", state: state)
|
await showError("Неожиданный ответ API.", state: state)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -607,7 +607,7 @@ final class ClaudeService {
|
|||||||
|
|
||||||
guard let textBlock = content.first(where: { $0["type"] as? String == "text" }),
|
guard let textBlock = content.first(where: { $0["type"] as? String == "text" }),
|
||||||
let text = textBlock["text"] as? String, !text.isEmpty else {
|
let text = textBlock["text"] as? String, !text.isEmpty else {
|
||||||
await showError("No response text.", state: state)
|
await showError("Ответ пустой.", state: state)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -627,7 +627,7 @@ final class ClaudeService {
|
|||||||
let content = json["content"] as? [[String: Any]],
|
let content = json["content"] as? [[String: Any]],
|
||||||
let textBlock = content.first(where: { $0["type"] as? String == "text" }),
|
let textBlock = content.first(where: { $0["type"] as? String == "text" }),
|
||||||
let text = textBlock["text"] as? String else {
|
let text = textBlock["text"] as? String else {
|
||||||
await showError("Unexpected API response.", state: state)
|
await showError("Неожиданный ответ API.", state: state)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -642,7 +642,7 @@ final class ClaudeService {
|
|||||||
// Try to parse as our JSON format
|
// Try to parse as our JSON format
|
||||||
if let resultData = cleanText.data(using: .utf8),
|
if let resultData = cleanText.data(using: .utf8),
|
||||||
let parsed = try? JSONSerialization.jsonObject(with: resultData) as? [String: Any] {
|
let parsed = try? JSONSerialization.jsonObject(with: resultData) as? [String: Any] {
|
||||||
let title = parsed["title"] as? String ?? "Result"
|
let title = parsed["title"] as? String ?? "Результат"
|
||||||
let note = parsed["note"] as? String
|
let note = parsed["note"] as? String
|
||||||
var items: [ResultItem] = []
|
var items: [ResultItem] = []
|
||||||
if let rawItems = parsed["items"] as? [[String: Any]] {
|
if let rawItems = parsed["items"] as? [[String: Any]] {
|
||||||
@@ -659,7 +659,7 @@ final class ClaudeService {
|
|||||||
// Fallback: show raw text in 3-line chunks
|
// Fallback: show raw text in 3-line chunks
|
||||||
let lines = cleanText.components(separatedBy: "\n").filter { !$0.isEmpty }.prefix(3)
|
let lines = cleanText.components(separatedBy: "\n").filter { !$0.isEmpty }.prefix(3)
|
||||||
state.searchResult = SearchResult(
|
state.searchResult = SearchResult(
|
||||||
title: "Claude's response",
|
title: "Ответ Claude",
|
||||||
items: lines.map { ResultItem(label: $0, detail: "", url: nil) },
|
items: lines.map { ResultItem(label: $0, detail: "", url: nil) },
|
||||||
note: nil
|
note: nil
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -329,7 +329,7 @@ final class HookServer: @unchecked Sendable {
|
|||||||
?? "unknown"
|
?? "unknown"
|
||||||
let cwd = payload["cwd"] as? String ?? ""
|
let cwd = payload["cwd"] as? String ?? ""
|
||||||
let rawName = URL(fileURLWithPath: cwd).lastPathComponent
|
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.
|
// Determine which pill this event belongs to.
|
||||||
// coucou_agent must be lowercase, digits and hyphens, ≤ 24 chars.
|
// 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 {
|
if let pending = state.pendingApproval, agentId == pending.pillId {
|
||||||
let handledNote: String
|
let handledNote: String
|
||||||
switch pending.pillId {
|
switch pending.pillId {
|
||||||
case "agent_cursor": handledNote = "Handled in Cursor."
|
case "agent_cursor": handledNote = "Решено в Cursor."
|
||||||
case "agent_codex": handledNote = "Handled in Codex."
|
case "agent_codex": handledNote = "Решено в Codex."
|
||||||
default: handledNote = "Handled in VS Code."
|
default: handledNote = "Решено в VS Code."
|
||||||
}
|
}
|
||||||
var resolved = false
|
var resolved = false
|
||||||
switch name {
|
switch name {
|
||||||
@@ -461,7 +461,7 @@ final class HookServer: @unchecked Sendable {
|
|||||||
|
|
||||||
case "PostToolUseFailure":
|
case "PostToolUseFailure":
|
||||||
state.updateTask(id: agentId, state: .working)
|
state.updateTask(id: agentId, state: .working)
|
||||||
appendStep(id: agentId, step: "⚠ failed")
|
appendStep(id: agentId, step: "⚠ ошибка")
|
||||||
|
|
||||||
case "Notification":
|
case "Notification":
|
||||||
let message = payload["message"] as? String ?? ""
|
let message = payload["message"] as? String ?? ""
|
||||||
@@ -522,10 +522,10 @@ final class HookServer: @unchecked Sendable {
|
|||||||
state.removeTask(id: agentId)
|
state.removeTask(id: agentId)
|
||||||
|
|
||||||
case "SubagentStart":
|
case "SubagentStart":
|
||||||
appendStep(id: agentId, step: "+ subagent")
|
appendStep(id: agentId, step: "+ субагент")
|
||||||
|
|
||||||
case "SubagentStop":
|
case "SubagentStop":
|
||||||
appendStep(id: agentId, step: "• subagent done")
|
appendStep(id: agentId, step: "• субагент готов")
|
||||||
|
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
@@ -613,7 +613,7 @@ final class HookServer: @unchecked Sendable {
|
|||||||
?? "unknown"
|
?? "unknown"
|
||||||
let cwd = payload["cwd"] as? String ?? ""
|
let cwd = payload["cwd"] as? String ?? ""
|
||||||
let rawName = URL(fileURLWithPath: cwd).lastPathComponent
|
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 rawAgent = payload["coucou_agent"] as? String ?? ""
|
||||||
let termProgram = payload["term_program"] 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 }
|
guard let self, self.pendingApprovalFD == fd else { return }
|
||||||
let note: String
|
let note: String
|
||||||
switch capturedPillId {
|
switch capturedPillId {
|
||||||
case "agent_cursor": note = "Handled in Cursor."
|
case "agent_cursor": note = "Решено в Cursor."
|
||||||
case "agent_codex": note = "Handled in Codex."
|
case "agent_codex": note = "Решено в Codex."
|
||||||
default: note = "Handled in VS Code."
|
default: note = "Решено в VS Code."
|
||||||
}
|
}
|
||||||
self.dismissApprovalCard(note: note)
|
self.dismissApprovalCard(note: note)
|
||||||
}
|
}
|
||||||
@@ -727,9 +727,9 @@ final class HookServer: @unchecked Sendable {
|
|||||||
guard let self, self.pendingApprovalFD == captured else { return }
|
guard let self, self.pendingApprovalFD == captured else { return }
|
||||||
let note: String
|
let note: String
|
||||||
switch capturedPillId {
|
switch capturedPillId {
|
||||||
case "agent_cursor": note = "Still waiting in Cursor."
|
case "agent_cursor": note = "Всё ещё ждёт в Cursor."
|
||||||
case "agent_codex": note = "Still waiting in Codex."
|
case "agent_codex": note = "Всё ещё ждёт в Codex."
|
||||||
default: note = "Still waiting in VS Code."
|
default: note = "Всё ещё ждёт в VS Code."
|
||||||
}
|
}
|
||||||
self.dismissApprovalCard(note: note)
|
self.dismissApprovalCard(note: note)
|
||||||
}
|
}
|
||||||
@@ -789,7 +789,7 @@ final class HookServer: @unchecked Sendable {
|
|||||||
?? "unknown"
|
?? "unknown"
|
||||||
let cwd = payload["cwd"] as? String ?? ""
|
let cwd = payload["cwd"] as? String ?? ""
|
||||||
let rawName = URL(fileURLWithPath: cwd).lastPathComponent
|
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 rawAgent = payload["coucou_agent"] as? String ?? ""
|
||||||
let termProgram = payload["term_program"] as? String ?? ""
|
let termProgram = payload["term_program"] as? String ?? ""
|
||||||
@@ -930,27 +930,27 @@ final class HookServer: @unchecked Sendable {
|
|||||||
return aliases[name.lowercased()] ?? name
|
return aliases[name.lowercased()] ?? name
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - French step labels
|
// MARK: - Step labels (Russian)
|
||||||
|
|
||||||
private func frenchStep(tool: String, input: [String: Any]) -> String {
|
private func frenchStep(tool: String, input: [String: Any]) -> String {
|
||||||
let labels: [String: String] = [
|
let labels: [String: String] = [
|
||||||
"Bash": "Exécute",
|
"Bash": "Выполняет",
|
||||||
"Read": "Lit",
|
"Read": "Читает",
|
||||||
"Write": "Écrit",
|
"Write": "Пишет",
|
||||||
"Edit": "Modifie",
|
"Edit": "Правит",
|
||||||
"Glob": "Cherche",
|
"Glob": "Ищет",
|
||||||
"Grep": "Recherche",
|
"Grep": "Ищет",
|
||||||
"WebSearch": "Recherche web",
|
"WebSearch": "Веб-поиск",
|
||||||
"WebFetch": "Récupère",
|
"WebFetch": "Загружает",
|
||||||
"TodoWrite": "Tâches",
|
"TodoWrite": "Задачи",
|
||||||
"Task": "Agent",
|
"Task": "Агент",
|
||||||
"LS": "Liste",
|
"LS": "Список",
|
||||||
"MultiEdit": "Modifie",
|
"MultiEdit": "Правит",
|
||||||
"NotebookEdit": "Notebook",
|
"NotebookEdit": "Блокнот",
|
||||||
// Codex tools
|
// Codex tools
|
||||||
"apply_patch": "Modifie",
|
"apply_patch": "Правит",
|
||||||
"update_plan": "Tâches",
|
"update_plan": "Задачи",
|
||||||
"spawn_agent": "Agent",
|
"spawn_agent": "Агент",
|
||||||
]
|
]
|
||||||
var label = labels[tool] ?? tool
|
var label = labels[tool] ?? tool
|
||||||
|
|
||||||
@@ -991,19 +991,19 @@ final class HookServer: @unchecked Sendable {
|
|||||||
return label
|
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 {
|
private func bashVerb(_ command: String) -> String {
|
||||||
let first = command.split(whereSeparator: { $0.isWhitespace }).first.map(String.init) ?? ""
|
let first = command.split(whereSeparator: { $0.isWhitespace }).first.map(String.init) ?? ""
|
||||||
switch first {
|
switch first {
|
||||||
case "cat", "bat", "head", "tail", "less", "more", "nl": return "Lit"
|
case "cat", "bat", "head", "tail", "less", "more", "nl": return "Читает"
|
||||||
case "rg", "grep", "find", "fd", "ls", "tree", "wc": return "Cherche"
|
case "rg", "grep", "find", "fd", "ls", "tree", "wc": return "Ищет"
|
||||||
default: break
|
default: break
|
||||||
}
|
}
|
||||||
let testRunners = ["pytest", "vitest", "jest", "npm test", "npm run test",
|
let testRunners = ["pytest", "vitest", "jest", "npm test", "npm run test",
|
||||||
"cargo test", "go test", "swift test", "make test",
|
"cargo test", "go test", "swift test", "make test",
|
||||||
"xcodebuild test", "unittest"]
|
"xcodebuild test", "unittest"]
|
||||||
if testRunners.contains(where: { command.contains($0) }) { return "Teste" }
|
if testRunners.contains(where: { command.contains($0) }) { return "Тестирует" }
|
||||||
return "Exécute"
|
return "Выполняет"
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Live diff helpers
|
// MARK: - Live diff helpers
|
||||||
@@ -1284,7 +1284,7 @@ final class HookServer: @unchecked Sendable {
|
|||||||
if clCheck.terminationStatus != 0 {
|
if clCheck.terminationStatus != 0 {
|
||||||
throw NSError(domain: "Coucou", code: 1,
|
throw NSError(domain: "Coucou", code: 1,
|
||||||
userInfo: [NSLocalizedDescriptionKey:
|
userInfo: [NSLocalizedDescriptionKey:
|
||||||
"Command Line Tools are required but not installed. Run: xcode-select --install"])
|
"Нужны Command Line Tools, но они не установлены. Выполните: xcode-select --install"])
|
||||||
}
|
}
|
||||||
|
|
||||||
if let existing = oldSL,
|
if let existing = oldSL,
|
||||||
@@ -1332,13 +1332,13 @@ final class HookServer: @unchecked Sendable {
|
|||||||
|
|
||||||
// Build a compact diff: show only the statusLine key before → after
|
// Build a compact diff: show only the statusLine key before → after
|
||||||
func slJSON(_ val: [String: Any]?) throws -> String {
|
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])
|
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 before = try slJSON(oldSL)
|
||||||
let after = try slJSON(newSL)
|
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).
|
/// 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)
|
let exists = FileManager.default.fileExists(atPath: url.path)
|
||||||
if !install && !exists {
|
if !install && !exists {
|
||||||
throw NSError(domain: "CoucouNoop", code: 0, userInfo: [
|
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()
|
let current = exists ? try Data(contentsOf: url) : Data()
|
||||||
@@ -1527,7 +1527,7 @@ final class HookServer: @unchecked Sendable {
|
|||||||
let current = (try? Data(contentsOf: url)) ?? Data()
|
let current = (try? Data(contentsOf: url)) ?? Data()
|
||||||
guard sha256Hex(current) == fp else {
|
guard sha256Hex(current) == fp else {
|
||||||
throw NSError(domain: "Coucou", code: 1, userInfo: [
|
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")
|
try writeJSONFile(data, to: url, suffix: "settings.json")
|
||||||
@@ -1540,7 +1540,7 @@ final class HookServer: @unchecked Sendable {
|
|||||||
label: "~/.gemini/settings.json")
|
label: "~/.gemini/settings.json")
|
||||||
if let raw = settings["hooks"], !(raw is [String: Any]) {
|
if let raw = settings["hooks"], !(raw is [String: Any]) {
|
||||||
throw NSError(domain: "Coucou", code: 2, userInfo: [
|
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()
|
let base = hookBase()
|
||||||
@@ -1557,7 +1557,7 @@ final class HookServer: @unchecked Sendable {
|
|||||||
for (geminiEvent, normalizedEvent, timeout) in events {
|
for (geminiEvent, normalizedEvent, timeout) in events {
|
||||||
if let raw = hooks[geminiEvent], !(raw is [[String: Any]]) {
|
if let raw = hooks[geminiEvent], !(raw is [[String: Any]]) {
|
||||||
throw NSError(domain: "Coucou", code: 2, userInfo: [
|
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]] ?? []
|
var groups = hooks[geminiEvent] as? [[String: Any]] ?? []
|
||||||
@@ -1581,7 +1581,7 @@ final class HookServer: @unchecked Sendable {
|
|||||||
label: "~/.gemini/settings.json")
|
label: "~/.gemini/settings.json")
|
||||||
if let raw = settings["hooks"], !(raw is [String: Any]) {
|
if let raw = settings["hooks"], !(raw is [String: Any]) {
|
||||||
throw NSError(domain: "Coucou", code: 2, userInfo: [
|
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] {
|
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)
|
let exists = FileManager.default.fileExists(atPath: url.path)
|
||||||
if !install && !exists {
|
if !install && !exists {
|
||||||
throw NSError(domain: "CoucouNoop", code: 0, userInfo: [
|
throw NSError(domain: "CoucouNoop", code: 0, userInfo: [
|
||||||
NSLocalizedDescriptionKey: "No Antigravity hooks to remove."
|
NSLocalizedDescriptionKey: "Нет хуков Antigravity для удаления."
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
let current = exists ? try Data(contentsOf: url) : Data()
|
let current = exists ? try Data(contentsOf: url) : Data()
|
||||||
@@ -1623,7 +1623,7 @@ final class HookServer: @unchecked Sendable {
|
|||||||
let current = (try? Data(contentsOf: url)) ?? Data()
|
let current = (try? Data(contentsOf: url)) ?? Data()
|
||||||
guard sha256Hex(current) == fp else {
|
guard sha256Hex(current) == fp else {
|
||||||
throw NSError(domain: "Coucou", code: 1, userInfo: [
|
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")
|
try writeJSONFile(data, to: url, suffix: "hooks.json")
|
||||||
@@ -1679,12 +1679,12 @@ final class HookServer: @unchecked Sendable {
|
|||||||
do { data = try Data(contentsOf: url) }
|
do { data = try Data(contentsOf: url) }
|
||||||
catch {
|
catch {
|
||||||
throw NSError(domain: "Coucou", code: 2, userInfo: [
|
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 {
|
guard let obj = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any] else {
|
||||||
throw NSError(domain: "Coucou", code: 2, userInfo: [
|
throw NSError(domain: "Coucou", code: 2, userInfo: [
|
||||||
NSLocalizedDescriptionKey: "\(label) is not valid JSON — BroV has not touched it."
|
NSLocalizedDescriptionKey: "\(label) — некорректный JSON, BroV его не трогал."
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
return obj
|
return obj
|
||||||
@@ -1702,7 +1702,7 @@ final class HookServer: @unchecked Sendable {
|
|||||||
do { try fm.copyItem(at: url, to: backupURL) }
|
do { try fm.copyItem(at: url, to: backupURL) }
|
||||||
catch {
|
catch {
|
||||||
throw NSError(domain: "Coucou", code: 3, userInfo: [
|
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)
|
let exists = FileManager.default.fileExists(atPath: url.path)
|
||||||
if !install && !exists {
|
if !install && !exists {
|
||||||
throw NSError(domain: "CoucouNoop", code: 0, userInfo: [
|
throw NSError(domain: "CoucouNoop", code: 0, userInfo: [
|
||||||
NSLocalizedDescriptionKey: "No Codex hooks to remove."
|
NSLocalizedDescriptionKey: "Нет хуков Codex для удаления."
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
let current = exists ? try Data(contentsOf: url) : Data()
|
let current = exists ? try Data(contentsOf: url) : Data()
|
||||||
@@ -1778,7 +1778,7 @@ final class HookServer: @unchecked Sendable {
|
|||||||
let current = (try? Data(contentsOf: url)) ?? Data()
|
let current = (try? Data(contentsOf: url)) ?? Data()
|
||||||
guard sha256Hex(current) == fp else {
|
guard sha256Hex(current) == fp else {
|
||||||
throw NSError(domain: "Coucou", code: 1, userInfo: [
|
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")
|
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")
|
var root = try Self.strictReadJSONObject(at: Self.codexHooksURL, label: "~/.codex/hooks.json")
|
||||||
if let raw = root["hooks"], !(raw is [String: Any]) {
|
if let raw = root["hooks"], !(raw is [String: Any]) {
|
||||||
throw NSError(domain: "Coucou", code: 2, userInfo: [
|
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()
|
let base = hookBase()
|
||||||
@@ -1812,7 +1812,7 @@ final class HookServer: @unchecked Sendable {
|
|||||||
for (event, timeout, statusMsg) in events {
|
for (event, timeout, statusMsg) in events {
|
||||||
if let raw = hooks[event], !(raw is [[String: Any]]) {
|
if let raw = hooks[event], !(raw is [[String: Any]]) {
|
||||||
throw NSError(domain: "Coucou", code: 2, userInfo: [
|
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]] ?? []
|
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")
|
var root = try Self.strictReadJSONObject(at: Self.codexHooksURL, label: "~/.codex/hooks.json")
|
||||||
if let raw = root["hooks"], !(raw is [String: Any]) {
|
if let raw = root["hooks"], !(raw is [String: Any]) {
|
||||||
throw NSError(domain: "Coucou", code: 2, userInfo: [
|
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] {
|
if var hooks = root["hooks"] as? [String: Any] {
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ struct OverviewView: View {
|
|||||||
Text({ () -> String in
|
Text({ () -> String in
|
||||||
switch agent.source {
|
switch agent.source {
|
||||||
case .claudeCode: return "Claude Code"
|
case .claudeCode: return "Claude Code"
|
||||||
case .agent: return "Agent"
|
case .agent: return "Агент"
|
||||||
case .n8n: return "n8n"
|
case .n8n: return "n8n"
|
||||||
}
|
}
|
||||||
}())
|
}())
|
||||||
@@ -272,14 +272,14 @@ struct EmptyStateView: View {
|
|||||||
CardBackground(wash: nil)
|
CardBackground(wash: nil)
|
||||||
HStack(spacing: 16) {
|
HStack(spacing: 16) {
|
||||||
VStack(alignment: .leading, spacing: 5) {
|
VStack(alignment: .leading, spacing: 5) {
|
||||||
Text("Nothing running right now.")
|
Text("Сейчас ничего не запущено.")
|
||||||
.font(.system(size: 15, weight: .semibold))
|
.font(.system(size: 15, weight: .semibold))
|
||||||
Text("Drop a file or window, or ask me anything.")
|
Text("Перетащи файл или окно — или спроси что угодно.")
|
||||||
.font(.system(size: 13))
|
.font(.system(size: 13))
|
||||||
.foregroundColor(Color(hex: "#9398A1"))
|
.foregroundColor(Color(hex: "#9398A1"))
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
PrimaryButton("Ask Claude") {
|
PrimaryButton("Спросить Claude") {
|
||||||
state.view = .prompt
|
state.view = .prompt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -300,18 +300,18 @@ struct ApprovalView: View {
|
|||||||
ZStack {
|
ZStack {
|
||||||
CardBackground(wash: .amber)
|
CardBackground(wash: .amber)
|
||||||
VStack(alignment: .leading, spacing: 5) {
|
VStack(alignment: .leading, spacing: 5) {
|
||||||
AgentWho(task: state.focusTask, label: "needs permission")
|
AgentWho(task: state.focusTask, label: "просит разрешение")
|
||||||
CodeBlock(text: approval?.command ?? approval?.tool ?? "…")
|
CodeBlock(text: approval?.command ?? approval?.tool ?? "…")
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
SecondaryButton("Deny") {
|
SecondaryButton("Запретить") {
|
||||||
HookServer.shared.sendApprovalDecision("deny")
|
HookServer.shared.sendApprovalDecision("deny")
|
||||||
}
|
}
|
||||||
PrimaryButton("Allow") {
|
PrimaryButton("Разрешить") {
|
||||||
HookServer.shared.sendApprovalDecision("allow")
|
HookServer.shared.sendApprovalDecision("allow")
|
||||||
}
|
}
|
||||||
// Codex rejects updatedPermissions, so "Always" is not offered
|
// Codex rejects updatedPermissions, so "Always" is not offered
|
||||||
if approval?.pillId != "agent_codex" {
|
if approval?.pillId != "agent_codex" {
|
||||||
SecondaryButton("Always") {
|
SecondaryButton("Всегда") {
|
||||||
HookServer.shared.sendApprovalDecision("always")
|
HookServer.shared.sendApprovalDecision("always")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -356,14 +356,14 @@ struct QuestionView: View {
|
|||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
// Header row: agent name + question counter + "Reply in terminal" link
|
// Header row: agent name + question counter + "Reply in terminal" link
|
||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
AgentWho(task: nil, label: "Claude Code is asking")
|
AgentWho(task: nil, label: "Claude Code спрашивает")
|
||||||
Spacer(minLength: 4)
|
Spacer(minLength: 4)
|
||||||
if q.questions.count > 1 {
|
if q.questions.count > 1 {
|
||||||
Text("\(qi + 1)/\(q.questions.count)")
|
Text("\(qi + 1)/\(q.questions.count)")
|
||||||
.font(.system(size: 10))
|
.font(.system(size: 10))
|
||||||
.foregroundColor(Color(hex: "#6B7079"))
|
.foregroundColor(Color(hex: "#6B7079"))
|
||||||
}
|
}
|
||||||
Button("Reply in terminal") { HookServer.shared.sendQuestionAsk() }
|
Button("Ответить в терминале") { HookServer.shared.sendQuestionAsk() }
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.font(.system(size: 10))
|
.font(.system(size: 10))
|
||||||
.foregroundColor(Color(hex: "#6B7079"))
|
.foregroundColor(Color(hex: "#6B7079"))
|
||||||
@@ -382,7 +382,7 @@ struct QuestionView: View {
|
|||||||
// Options (wrapping) or "Other…" compact inline row
|
// Options (wrapping) or "Other…" compact inline row
|
||||||
if curOther {
|
if curOther {
|
||||||
HStack(spacing: 6) {
|
HStack(spacing: 6) {
|
||||||
TextField("Your answer…", text: Binding(
|
TextField("Твой ответ…", text: Binding(
|
||||||
get: { qi < otherTexts.count ? otherTexts[qi] : "" },
|
get: { qi < otherTexts.count ? otherTexts[qi] : "" },
|
||||||
set: { v in if qi < otherTexts.count { otherTexts[qi] = v } }
|
set: { v in if qi < otherTexts.count { otherTexts[qi] = v } }
|
||||||
))
|
))
|
||||||
@@ -396,7 +396,7 @@ struct QuestionView: View {
|
|||||||
.padding(.horizontal, 8).padding(.vertical, 5)
|
.padding(.horizontal, 8).padding(.vertical, 5)
|
||||||
.background(Color.white.opacity(0.07))
|
.background(Color.white.opacity(0.07))
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||||
Button(isLast ? "Send" : "Next") {
|
Button(isLast ? "Отправить" : "Далее") {
|
||||||
commitOtherAndProceed(q: q, qi: qi, isLast: isLast)
|
commitOtherAndProceed(q: q, qi: qi, isLast: isLast)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
@@ -438,14 +438,14 @@ struct QuestionView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// "Other…" implicit free-text option
|
// "Other…" implicit free-text option
|
||||||
SecondaryButton("Other…") {
|
SecondaryButton("Другое…") {
|
||||||
if qi < showOther.count { showOther[qi] = true }
|
if qi < showOther.count { showOther[qi] = true }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Send/Next — only for multi-select (and not while "Other…" field is open)
|
// Send/Next — only for multi-select (and not while "Other…" field is open)
|
||||||
if isMulti && !curOther {
|
if isMulti && !curOther {
|
||||||
PrimaryButton(isLast ? "Send" : "Next") {
|
PrimaryButton(isLast ? "Отправить" : "Далее") {
|
||||||
proceedFromQuestion(q: q, qi: qi, isLast: isLast)
|
proceedFromQuestion(q: q, qi: qi, isLast: isLast)
|
||||||
}
|
}
|
||||||
.disabled(!canProceed)
|
.disabled(!canProceed)
|
||||||
@@ -521,14 +521,14 @@ struct ErrorView: View {
|
|||||||
CardBackground(wash: .red)
|
CardBackground(wash: .red)
|
||||||
VStack(alignment: .leading, spacing: 5) {
|
VStack(alignment: .leading, spacing: 5) {
|
||||||
AgentWho(task: state.focusTask, label: "n8n")
|
AgentWho(task: state.focusTask, label: "n8n")
|
||||||
Text("Workflow stopped.")
|
Text("Сценарий остановлен.")
|
||||||
.font(.system(size: 15, weight: .semibold))
|
.font(.system(size: 15, weight: .semibold))
|
||||||
Text("Gmail node timed out after 30s. Retry or open n8n.")
|
Text("Узел Gmail не ответил за 30 с. Повтори или открой n8n.")
|
||||||
.font(.system(size: 12))
|
.font(.system(size: 12))
|
||||||
.foregroundColor(Color(hex: "#FF8D97"))
|
.foregroundColor(Color(hex: "#FF8D97"))
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
PrimaryButton("Retry") { /* retry */ }
|
PrimaryButton("Повторить") { /* retry */ }
|
||||||
SecondaryButton("Open in n8n") { /* open */ }
|
SecondaryButton("Открыть в n8n") { /* open */ }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.leading, 116)
|
.padding(.leading, 116)
|
||||||
@@ -548,18 +548,18 @@ struct FinishedView: View {
|
|||||||
ZStack {
|
ZStack {
|
||||||
CardBackground(wash: .green)
|
CardBackground(wash: .green)
|
||||||
VStack(alignment: .leading, spacing: 5) {
|
VStack(alignment: .leading, spacing: 5) {
|
||||||
AgentWho(task: state.focusTask, label: "Claude Code finished")
|
AgentWho(task: state.focusTask, label: "Claude Code закончил")
|
||||||
Text({
|
Text({
|
||||||
if let fl = state.focusTask?.finalLine { return fl }
|
if let fl = state.focusTask?.finalLine { return fl }
|
||||||
if let s = state.focusTask?.steps.last(where: { !$0.isDiffStep }) { return s }
|
if let s = state.focusTask?.steps.last(where: { !$0.isDiffStep }) { return s }
|
||||||
return "Session finished"
|
return "Сессия завершена"
|
||||||
}())
|
}())
|
||||||
.font(.system(size: 15, weight: .semibold))
|
.font(.system(size: 15, weight: .semibold))
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.truncationMode(.tail)
|
.truncationMode(.tail)
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
#if !APPSTORE
|
#if !APPSTORE
|
||||||
PrimaryButton("Open terminal") {
|
PrimaryButton("Открыть терминал") {
|
||||||
let terminalBundleIds = ["com.apple.Terminal", "com.googlecode.iterm2", "net.kovidgoyal.kitty", "com.mitchellh.ghostty"]
|
let terminalBundleIds = ["com.apple.Terminal", "com.googlecode.iterm2", "net.kovidgoyal.kitty", "com.mitchellh.ghostty"]
|
||||||
let activated = terminalBundleIds.compactMap { id in
|
let activated = terminalBundleIds.compactMap { id in
|
||||||
NSWorkspace.shared.runningApplications.first { $0.bundleIdentifier == id }
|
NSWorkspace.shared.runningApplications.first { $0.bundleIdentifier == id }
|
||||||
@@ -570,7 +570,7 @@ struct FinishedView: View {
|
|||||||
NotificationCenter.default.post(name: .islandCollapse, object: nil)
|
NotificationCenter.default.post(name: .islandCollapse, object: nil)
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
SecondaryButton("OK") {
|
SecondaryButton("ОК") {
|
||||||
NotificationCenter.default.post(name: .islandCollapse, object: nil)
|
NotificationCenter.default.post(name: .islandCollapse, object: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -632,11 +632,11 @@ struct DiffCardView: View {
|
|||||||
|
|
||||||
// Content
|
// Content
|
||||||
if diff.tooLarge {
|
if diff.tooLarge {
|
||||||
Text("Diff too large")
|
Text("Слишком большой diff")
|
||||||
.font(.system(size: 10.5))
|
.font(.system(size: 10.5))
|
||||||
.foregroundColor(Color(hex: "#6B7079"))
|
.foregroundColor(Color(hex: "#6B7079"))
|
||||||
} else if allLines.isEmpty {
|
} else if allLines.isEmpty {
|
||||||
Text("No changes")
|
Text("Нет изменений")
|
||||||
.font(.system(size: 10.5))
|
.font(.system(size: 10.5))
|
||||||
.foregroundColor(Color(hex: "#6B7079"))
|
.foregroundColor(Color(hex: "#6B7079"))
|
||||||
} else {
|
} else {
|
||||||
@@ -726,8 +726,8 @@ struct ConfusedView: View {
|
|||||||
ZStack {
|
ZStack {
|
||||||
CardBackground(wash: .pink)
|
CardBackground(wash: .pink)
|
||||||
VStack(alignment: .leading, spacing: 5) {
|
VStack(alignment: .leading, spacing: 5) {
|
||||||
Text("Too many hits at once.").font(.system(size: 15, weight: .semibold))
|
Text("Слишком много тычков разом.").font(.system(size: 15, weight: .semibold))
|
||||||
Text("Give me a sec — back to work in three seconds.")
|
Text("Дай секунду — вернусь к работе через три секунды.")
|
||||||
.font(.system(size: 13)).foregroundColor(Color(hex: "#9398A1"))
|
.font(.system(size: 13)).foregroundColor(Color(hex: "#9398A1"))
|
||||||
}
|
}
|
||||||
.padding(.leading, 128)
|
.padding(.leading, 128)
|
||||||
@@ -768,11 +768,11 @@ struct UploadView: View {
|
|||||||
center: .bottom, startRadius: 0, endRadius: 200
|
center: .bottom, startRadius: 0, endRadius: 200
|
||||||
))
|
))
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
Text("Drop your files here")
|
Text("Перетащи файлы сюда")
|
||||||
.font(.system(size: 13, weight: .medium))
|
.font(.system(size: 13, weight: .medium))
|
||||||
.foregroundColor(state.fileDragOver ? Color(hex: "#34D399") : Color(hex: "#D5D7DB"))
|
.foregroundColor(state.fileDragOver ? Color(hex: "#34D399") : Color(hex: "#D5D7DB"))
|
||||||
HStack(spacing: 6) {
|
HStack(spacing: 6) {
|
||||||
ForEach(["PDF", "Images", "Code", "Docs"], id: \.self) { label in
|
ForEach(["PDF", "Фото", "Код", "Документы"], id: \.self) { label in
|
||||||
Text(label)
|
Text(label)
|
||||||
.font(.system(size: 11))
|
.font(.system(size: 11))
|
||||||
.padding(.horizontal, 8).padding(.vertical, 3)
|
.padding(.horizontal, 8).padding(.vertical, 3)
|
||||||
@@ -879,12 +879,12 @@ struct UploadingView: View {
|
|||||||
Image(systemName: "checkmark.circle.fill")
|
Image(systemName: "checkmark.circle.fill")
|
||||||
.font(.system(size: 12))
|
.font(.system(size: 12))
|
||||||
.foregroundColor(Color(hex: "#34D399"))
|
.foregroundColor(Color(hex: "#34D399"))
|
||||||
Text(" \(state.droppedFile?.name ?? "File")")
|
Text(" \(state.droppedFile?.name ?? "Файл")")
|
||||||
.font(.system(size: 12.5, weight: .semibold))
|
.font(.system(size: 12.5, weight: .semibold))
|
||||||
.foregroundColor(Color(hex: "#34D399"))
|
.foregroundColor(Color(hex: "#34D399"))
|
||||||
.lineLimit(1).truncationMode(.middle)
|
.lineLimit(1).truncationMode(.middle)
|
||||||
} else {
|
} else {
|
||||||
Text("Uploading \(state.droppedFile?.name ?? "file")")
|
Text("Загружаю \(state.droppedFile?.name ?? "файл")")
|
||||||
.font(.system(size: 12.5))
|
.font(.system(size: 12.5))
|
||||||
.foregroundColor(Color(hex: "#A9ADB5"))
|
.foregroundColor(Color(hex: "#A9ADB5"))
|
||||||
.lineLimit(1).truncationMode(.middle)
|
.lineLimit(1).truncationMode(.middle)
|
||||||
@@ -915,12 +915,12 @@ struct ChooseView: View {
|
|||||||
ZStack(alignment: .leading) {
|
ZStack(alignment: .leading) {
|
||||||
CardBackground(wash: nil)
|
CardBackground(wash: nil)
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
let fileName = state.droppedFile?.name ?? "file"
|
let fileName = state.droppedFile?.name ?? "Файл"
|
||||||
(Text(fileName).font(.system(size: 14, weight: .semibold)) + Text(" is ready.").font(.system(size: 14, weight: .semibold)))
|
(Text(fileName).font(.system(size: 14, weight: .semibold)) + Text(" готов.").font(.system(size: 14, weight: .semibold)))
|
||||||
Text("What do you want to do with it?").font(.system(size: 12.5)).foregroundColor(Color(hex: "#9398A1"))
|
Text("Что с ним сделать?").font(.system(size: 12.5)).foregroundColor(Color(hex: "#9398A1"))
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
PrimaryButton("Ask a question") { state.view = .prompt }
|
PrimaryButton("Задать вопрос") { state.view = .prompt }
|
||||||
SecondaryButton("Send by email") { state.view = .mail }
|
SecondaryButton("Отправить почтой") { state.view = .mail }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.leading, 98)
|
.padding(.leading, 98)
|
||||||
@@ -944,7 +944,7 @@ struct MailView: View {
|
|||||||
CardBackground(wash: nil)
|
CardBackground(wash: nil)
|
||||||
VStack(alignment: .leading, spacing: 6) {
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
HStack(spacing: 6) {
|
HStack(spacing: 6) {
|
||||||
Text("New email").font(.system(size: 12, weight: .semibold))
|
Text("Новое письмо").font(.system(size: 12, weight: .semibold))
|
||||||
if let name = state.droppedFile?.name {
|
if let name = state.droppedFile?.name {
|
||||||
Text("with").font(.system(size: 12)).foregroundColor(Color(hex: "#8E939C"))
|
Text("with").font(.system(size: 12)).foregroundColor(Color(hex: "#8E939C"))
|
||||||
Text(name).font(.system(size: 12)).foregroundColor(Color(hex: "#8E939C"))
|
Text(name).font(.system(size: 12)).foregroundColor(Color(hex: "#8E939C"))
|
||||||
@@ -952,8 +952,8 @@ struct MailView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MailField(label: "To", placeholder: "address@example.com", text: $to)
|
MailField(label: "Кому", placeholder: "address@example.com", text: $to)
|
||||||
MailField(label: "Subject", placeholder: state.droppedFile?.name ?? "Subject", text: $subject)
|
MailField(label: "Тема", placeholder: state.droppedFile?.name ?? "Тема", text: $subject)
|
||||||
|
|
||||||
// Body — TextEditor scrolls internally when text overflows
|
// Body — TextEditor scrolls internally when text overflows
|
||||||
TextEditor(text: $bodyText)
|
TextEditor(text: $bodyText)
|
||||||
@@ -971,11 +971,11 @@ struct MailView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
PrimaryButton(isSending ? "Sending…" : "Send") {
|
PrimaryButton(isSending ? "Отправка…" : "Отправить") {
|
||||||
guard !isSending else { return }
|
guard !isSending else { return }
|
||||||
sendMail()
|
sendMail()
|
||||||
}
|
}
|
||||||
SecondaryButton("Cancel") { state.view = .choose }
|
SecondaryButton("Отмена") { state.view = .choose }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.leading, 92)
|
.padding(.leading, 92)
|
||||||
@@ -986,8 +986,8 @@ struct MailView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func sendMail() {
|
private func sendMail() {
|
||||||
guard !to.isEmpty else { statusMsg = "Missing recipient."; return }
|
guard !to.isEmpty else { statusMsg = "Не указан получатель."; return }
|
||||||
let subj = subject.isEmpty ? (state.droppedFile?.name ?? "File") : subject
|
let subj = subject.isEmpty ? (state.droppedFile?.name ?? "Файл") : subject
|
||||||
|
|
||||||
// Prefer Resend if API key + sender address are configured
|
// Prefer Resend if API key + sender address are configured
|
||||||
let apiKey = KeychainStore.shared.get("resend-api-key")
|
let apiKey = KeychainStore.shared.get("resend-api-key")
|
||||||
@@ -1006,12 +1006,12 @@ struct MailView: View {
|
|||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
isSending = false
|
isSending = false
|
||||||
if ok { onSuccess(recipient: recipient) }
|
if ok { onSuccess(recipient: recipient) }
|
||||||
else { statusMsg = "Resend error — check API key & sender." }
|
else { statusMsg = "Ошибка Resend — проверь API-ключ и отправителя." }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if apiKey != nil && fromAddr == nil {
|
} else if apiKey != nil && fromAddr == nil {
|
||||||
// API key set but no sender — guide user instead of silent fallback
|
// API key set but no sender — guide user instead of silent fallback
|
||||||
statusMsg = "Set sender address in Settings."
|
statusMsg = "Укажи адрес отправителя в настройках."
|
||||||
} else {
|
} else {
|
||||||
// No Resend — fallback to Mail
|
// No Resend — fallback to Mail
|
||||||
sendViaAppleMail(to: to, subject: subj)
|
sendViaAppleMail(to: to, subject: subj)
|
||||||
@@ -1051,7 +1051,7 @@ struct MailView: View {
|
|||||||
#if APPSTORE
|
#if APPSTORE
|
||||||
// App Store: no AppleScript — use NSSharingService to compose (user sends manually)
|
// App Store: no AppleScript — use NSSharingService to compose (user sends manually)
|
||||||
guard let service = NSSharingService(named: .composeEmail) else {
|
guard let service = NSSharingService(named: .composeEmail) else {
|
||||||
statusMsg = "Mail not available."
|
statusMsg = "Mail недоступен."
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var items: [Any] = [bodyText.isEmpty ? " " : bodyText]
|
var items: [Any] = [bodyText.isEmpty ? " " : bodyText]
|
||||||
@@ -1097,14 +1097,14 @@ struct MailView: View {
|
|||||||
var err: NSDictionary?
|
var err: NSDictionary?
|
||||||
NSAppleScript(source: script)?.executeAndReturnError(&err)
|
NSAppleScript(source: script)?.executeAndReturnError(&err)
|
||||||
if err == nil { onSuccess(recipient: to) }
|
if err == nil { onSuccess(recipient: to) }
|
||||||
else { statusMsg = "Mail error: \(err?["NSAppleScriptErrorMessage"] as? String ?? "unknown")" }
|
else { statusMsg = "Ошибка Mail: \(err?["NSAppleScriptErrorMessage"] as? String ?? "неизвестно")" }
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
private func onSuccess(recipient: String) {
|
private func onSuccess(recipient: String) {
|
||||||
SoundEngine.shared.play("send")
|
SoundEngine.shared.play("send")
|
||||||
NotificationCenter.default.post(name: .triggerEmote, object: BotEmote.wink)
|
NotificationCenter.default.post(name: .triggerEmote, object: BotEmote.wink)
|
||||||
state.noteMessage = "Email sent to \(recipient)."
|
state.noteMessage = "Письмо отправлено: \(recipient)."
|
||||||
state.view = .note
|
state.view = .note
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
||||||
NotificationCenter.default.post(name: .islandCollapse, object: nil)
|
NotificationCenter.default.post(name: .islandCollapse, object: nil)
|
||||||
@@ -1198,7 +1198,7 @@ struct PromptView: View {
|
|||||||
.padding(.horizontal, 10)
|
.padding(.horizontal, 10)
|
||||||
|
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
TextField(state.chatHistory.isEmpty ? "Ask me anything…" : "Continue…", text: $text)
|
TextField(state.chatHistory.isEmpty ? "Спроси что угодно…" : "Продолжить…", text: $text)
|
||||||
.textFieldStyle(.plain)
|
.textFieldStyle(.plain)
|
||||||
.font(.system(size: 13))
|
.font(.system(size: 13))
|
||||||
.focused($focused)
|
.focused($focused)
|
||||||
@@ -1368,7 +1368,7 @@ struct ModelPickerView: View {
|
|||||||
if state.loadingProviderModels.contains(state.chatProvider) {
|
if state.loadingProviderModels.contains(state.chatProvider) {
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
ProgressView().scaleEffect(0.7)
|
ProgressView().scaleEffect(0.7)
|
||||||
Text("Loading models…")
|
Text("Загрузка моделей…")
|
||||||
.font(.system(size: 12))
|
.font(.system(size: 12))
|
||||||
.foregroundColor(Color(hex: "#8A8F98"))
|
.foregroundColor(Color(hex: "#8A8F98"))
|
||||||
}
|
}
|
||||||
@@ -1476,9 +1476,9 @@ struct SearchingView: View {
|
|||||||
|
|
||||||
var label: String {
|
var label: String {
|
||||||
switch state.promptContext {
|
switch state.promptContext {
|
||||||
case .window(_, let title, _): return "Claude is reading \(title)…"
|
case .window(_, let title, _): return "Claude читает \(title)…"
|
||||||
case .file(let name, _): return "Claude is reading \(name)…"
|
case .file(let name, _): return "Claude читает \(name)…"
|
||||||
case nil: return "Claude is searching…"
|
case nil: return "Claude ищет…"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1534,17 +1534,17 @@ struct ResultView: View {
|
|||||||
// The URL comes from the model, which may have read attacker-controlled
|
// The URL comes from the model, which may have read attacker-controlled
|
||||||
// files or pages: only plain web links may leave the app.
|
// files or pages: only plain web links may leave the app.
|
||||||
let openURL = safeWebURL(result.items.first?.url)
|
let openURL = safeWebURL(result.items.first?.url)
|
||||||
PrimaryButton("Open") {
|
PrimaryButton("Открыть") {
|
||||||
if let openURL { NSWorkspace.shared.open(openURL) }
|
if let openURL { NSWorkspace.shared.open(openURL) }
|
||||||
}
|
}
|
||||||
.disabled(openURL == nil)
|
.disabled(openURL == nil)
|
||||||
.help(openURL?.absoluteString ?? "")
|
.help(openURL?.absoluteString ?? "")
|
||||||
SecondaryButton("Copy") {
|
SecondaryButton("Копировать") {
|
||||||
let text = result.items.map { "\($0.label): \($0.detail)" }.joined(separator: "\n")
|
let text = result.items.map { "\($0.label): \($0.detail)" }.joined(separator: "\n")
|
||||||
NSPasteboard.general.clearContents()
|
NSPasteboard.general.clearContents()
|
||||||
NSPasteboard.general.setString(text, forType: .string)
|
NSPasteboard.general.setString(text, forType: .string)
|
||||||
}
|
}
|
||||||
SecondaryButton("Close") { state.view = state.tasks.isEmpty ? .empty : .overview }
|
SecondaryButton("Закрыть") { state.view = state.tasks.isEmpty ? .empty : .overview }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.leading, 84)
|
.padding(.leading, 84)
|
||||||
@@ -1726,12 +1726,12 @@ struct IntegrationCardView: View {
|
|||||||
private var statusLabel: String {
|
private var statusLabel: String {
|
||||||
#if !APPSTORE
|
#if !APPSTORE
|
||||||
if task.id == "integration_music" {
|
if task.id == "integration_music" {
|
||||||
if appState.musicAutomationDenied { return "Automation not allowed" }
|
if appState.musicAutomationDenied { return "Автоматизация запрещена" }
|
||||||
if appState.musicPlaying { return "Playing · \(MusicController.shared.trackTitle ?? "Unknown")" }
|
if appState.musicPlaying { return "Играет · \(MusicController.shared.trackTitle ?? "Неизвестно")" }
|
||||||
return "Not playing"
|
return "Не играет"
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
if PillCatalog.definition(for: task.id)?.comingSoon == true { return "Coming soon" }
|
if PillCatalog.definition(for: task.id)?.comingSoon == true { return "Скоро" }
|
||||||
let svcErr = task.id == "integration_stripe" ? appState.stripeError
|
let svcErr = task.id == "integration_stripe" ? appState.stripeError
|
||||||
: task.id == "integration_calcom" ? appState.calcomError
|
: task.id == "integration_calcom" ? appState.calcomError
|
||||||
: nil
|
: nil
|
||||||
@@ -1739,12 +1739,12 @@ struct IntegrationCardView: View {
|
|||||||
let isHooks = task.id == "agent_gemini" || task.id == "agent_antigravity"
|
let isHooks = task.id == "agent_gemini" || task.id == "agent_antigravity"
|
||||||
let isAI = ChatProvider(pillID: task.id) != nil
|
let isAI = ChatProvider(pillID: task.id) != nil
|
||||||
if isConfigured {
|
if isConfigured {
|
||||||
if isHooks { return "Hooks installed" }
|
if isHooks { return "Хуки установлены" }
|
||||||
if isAI {
|
if isAI {
|
||||||
let provider = ChatProvider(pillID: task.id)!
|
let provider = ChatProvider(pillID: task.id)!
|
||||||
if provider.isLocal {
|
if provider.isLocal {
|
||||||
let model = provider == .ollama ? appState.ollamaChatModel : appState.lmstudioChatModel
|
let model = provider == .ollama ? appState.ollamaChatModel : appState.lmstudioChatModel
|
||||||
return "Connected · \(model)"
|
return "Подключено · \(model)"
|
||||||
}
|
}
|
||||||
let model: String
|
let model: String
|
||||||
switch task.id {
|
switch task.id {
|
||||||
@@ -1753,16 +1753,16 @@ struct IntegrationCardView: View {
|
|||||||
case "ai_openai": model = appState.openAIChatModel
|
case "ai_openai": model = appState.openAIChatModel
|
||||||
default: model = ""
|
default: model = ""
|
||||||
}
|
}
|
||||||
return "Key configured · \(model)"
|
return "Ключ задан · \(model)"
|
||||||
}
|
}
|
||||||
return "Connected · loading…"
|
return "Подключено · загрузка…"
|
||||||
} else {
|
} else {
|
||||||
if isHooks { return "Hooks not installed" }
|
if isHooks { return "Хуки не установлены" }
|
||||||
if isAI {
|
if isAI {
|
||||||
let provider = ChatProvider(pillID: task.id)!
|
let provider = ChatProvider(pillID: task.id)!
|
||||||
return provider.isLocal ? "Not connected" : "Key not configured"
|
return provider.isLocal ? "Не подключено" : "Ключ не задан"
|
||||||
}
|
}
|
||||||
return "Key not configured"
|
return "Ключ не задан"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1836,7 +1836,7 @@ struct IntegrationCardView: View {
|
|||||||
.foregroundColor(Color(hex: "#F5F6F8"))
|
.foregroundColor(Color(hex: "#F5F6F8"))
|
||||||
.lineLimit(1).truncationMode(.tail)
|
.lineLimit(1).truncationMode(.tail)
|
||||||
.layoutPriority(1)
|
.layoutPriority(1)
|
||||||
Text(PillCatalog.definition(for: task.id)?.sessionSubtitle ?? "Agent")
|
Text(PillCatalog.definition(for: task.id)?.sessionSubtitle ?? "Агент")
|
||||||
.font(.system(size: 11))
|
.font(.system(size: 11))
|
||||||
.foregroundColor(Color(hex: "#8E939C"))
|
.foregroundColor(Color(hex: "#8E939C"))
|
||||||
.lineLimit(1).truncationMode(.tail)
|
.lineLimit(1).truncationMode(.tail)
|
||||||
@@ -1870,7 +1870,7 @@ struct IntegrationCardView: View {
|
|||||||
Text(PillCatalog.definition(for: task.id)?.name ?? task.name)
|
Text(PillCatalog.definition(for: task.id)?.name ?? task.name)
|
||||||
.font(.system(size: 12, weight: .semibold))
|
.font(.system(size: 12, weight: .semibold))
|
||||||
.foregroundColor(Color(hex: "#F5F6F8"))
|
.foregroundColor(Color(hex: "#F5F6F8"))
|
||||||
Text(PillCatalog.definition(for: task.id)?.subtitle ?? "Integration")
|
Text(PillCatalog.definition(for: task.id)?.subtitle ?? "Интеграция")
|
||||||
.font(.system(size: 11))
|
.font(.system(size: 11))
|
||||||
.foregroundColor(Color(hex: "#8E939C"))
|
.foregroundColor(Color(hex: "#8E939C"))
|
||||||
Spacer(minLength: 2)
|
Spacer(minLength: 2)
|
||||||
@@ -1890,7 +1890,7 @@ struct IntegrationCardView: View {
|
|||||||
|
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
if task.id == "integration_claude" {
|
if task.id == "integration_claude" {
|
||||||
Button("Open Visual Studio Code") { openVSCode() }
|
Button("Открыть Visual Studio Code") { openVSCode() }
|
||||||
.font(.system(size: 11, weight: .medium))
|
.font(.system(size: 11, weight: .medium))
|
||||||
.foregroundColor(Color(hex: task.color).opacity(0.7))
|
.foregroundColor(Color(hex: task.color).opacity(0.7))
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
@@ -1898,7 +1898,7 @@ struct IntegrationCardView: View {
|
|||||||
#if !APPSTORE
|
#if !APPSTORE
|
||||||
if let url = NSWorkspace.shared.urlForApplication(
|
if let url = NSWorkspace.shared.urlForApplication(
|
||||||
withBundleIdentifier: "com.todesktop.230313mzl4w4u92") {
|
withBundleIdentifier: "com.todesktop.230313mzl4w4u92") {
|
||||||
Button("Open Cursor") {
|
Button("Открыть Cursor") {
|
||||||
NSWorkspace.shared.openApplication(at: url, configuration: .init(),
|
NSWorkspace.shared.openApplication(at: url, configuration: .init(),
|
||||||
completionHandler: nil)
|
completionHandler: nil)
|
||||||
}
|
}
|
||||||
@@ -1911,7 +1911,7 @@ struct IntegrationCardView: View {
|
|||||||
#if !APPSTORE
|
#if !APPSTORE
|
||||||
if let url = NSWorkspace.shared.urlForApplication(
|
if let url = NSWorkspace.shared.urlForApplication(
|
||||||
withBundleIdentifier: "com.openai.codex") {
|
withBundleIdentifier: "com.openai.codex") {
|
||||||
Button("Open Codex") {
|
Button("Открыть Codex") {
|
||||||
NSWorkspace.shared.openApplication(at: url, configuration: .init(),
|
NSWorkspace.shared.openApplication(at: url, configuration: .init(),
|
||||||
completionHandler: nil)
|
completionHandler: nil)
|
||||||
}
|
}
|
||||||
@@ -1922,7 +1922,7 @@ struct IntegrationCardView: View {
|
|||||||
#endif
|
#endif
|
||||||
} else if let provider = ChatProvider(pillID: task.id) {
|
} else if let provider = ChatProvider(pillID: task.id) {
|
||||||
if isConfigured {
|
if isConfigured {
|
||||||
Button("Chat with \(task.name)") {
|
Button("Чат с \(task.name)") {
|
||||||
switchChatProvider(provider)
|
switchChatProvider(provider)
|
||||||
}
|
}
|
||||||
.font(.system(size: 11, weight: .medium))
|
.font(.system(size: 11, weight: .medium))
|
||||||
@@ -1931,12 +1931,12 @@ struct IntegrationCardView: View {
|
|||||||
}
|
}
|
||||||
} else if task.id == "integration_music" {
|
} else if task.id == "integration_music" {
|
||||||
#if !APPSTORE
|
#if !APPSTORE
|
||||||
Button("Open Music") { MusicController.shared.openMusic() }
|
Button("Открыть Музыку") { MusicController.shared.openMusic() }
|
||||||
.font(.system(size: 11, weight: .medium))
|
.font(.system(size: 11, weight: .medium))
|
||||||
.foregroundColor(Color(hex: task.color).opacity(0.85))
|
.foregroundColor(Color(hex: task.color).opacity(0.85))
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
if appState.musicAutomationDenied {
|
if appState.musicAutomationDenied {
|
||||||
Button("Open Settings…") { MusicController.shared.openAutomationSettings() }
|
Button("Открыть настройки…") { MusicController.shared.openAutomationSettings() }
|
||||||
.font(.system(size: 11))
|
.font(.system(size: 11))
|
||||||
.foregroundColor(Color(hex: "#8E939C"))
|
.foregroundColor(Color(hex: "#8E939C"))
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
@@ -1951,7 +1951,7 @@ struct IntegrationCardView: View {
|
|||||||
}) {
|
}) {
|
||||||
HStack(spacing: 5) {
|
HStack(spacing: 5) {
|
||||||
Circle().fill(accent).frame(width: 5, height: 5)
|
Circle().fill(accent).frame(width: 5, height: 5)
|
||||||
Text(task.steps.first ?? "Workflow")
|
Text(task.steps.first ?? "Сценарий")
|
||||||
.font(.system(size: 11))
|
.font(.system(size: 11))
|
||||||
.foregroundColor(Color(hex: "#C5C8CD"))
|
.foregroundColor(Color(hex: "#C5C8CD"))
|
||||||
.lineLimit(1).truncationMode(.tail)
|
.lineLimit(1).truncationMode(.tail)
|
||||||
@@ -1966,19 +1966,19 @@ struct IntegrationCardView: View {
|
|||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
} else if let url = openURL {
|
} else if let url = openURL {
|
||||||
Button("Open \(task.name)") { NSWorkspace.shared.open(url) }
|
Button("Открыть \(task.name)") { NSWorkspace.shared.open(url) }
|
||||||
.font(.system(size: 11, weight: .medium))
|
.font(.system(size: 11, weight: .medium))
|
||||||
.foregroundColor(Color(hex: task.color).opacity(0.85))
|
.foregroundColor(Color(hex: task.color).opacity(0.85))
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
if task.id == "integration_stripe" && isConfigured {
|
if task.id == "integration_stripe" && isConfigured {
|
||||||
Button("Refresh") { Task { @MainActor in StripePoller.shared.pollNow() } }
|
Button("Обновить") { Task { @MainActor in StripePoller.shared.pollNow() } }
|
||||||
.font(.system(size: 11, weight: .medium))
|
.font(.system(size: 11, weight: .medium))
|
||||||
.foregroundColor(Color(hex: "#0570DE").opacity(0.85))
|
.foregroundColor(Color(hex: "#0570DE").opacity(0.85))
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
if task.id == "integration_calcom" && isConfigured {
|
if task.id == "integration_calcom" && isConfigured {
|
||||||
Button("Refresh") { Task { @MainActor in CalcomPoller.shared.pollNow() } }
|
Button("Обновить") { Task { @MainActor in CalcomPoller.shared.pollNow() } }
|
||||||
.font(.system(size: 11, weight: .medium))
|
.font(.system(size: 11, weight: .medium))
|
||||||
.foregroundColor(Color(hex: "#C9956A").opacity(0.85))
|
.foregroundColor(Color(hex: "#C9956A").opacity(0.85))
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
@@ -1988,7 +1988,7 @@ struct IntegrationCardView: View {
|
|||||||
&& task.id != "agent_cursor"
|
&& task.id != "agent_cursor"
|
||||||
&& task.id != "agent_codex"
|
&& task.id != "agent_codex"
|
||||||
&& task.id != "integration_music" {
|
&& task.id != "integration_music" {
|
||||||
Button("Settings…") {
|
Button("Настройки…") {
|
||||||
let section: String
|
let section: String
|
||||||
switch PillCatalog.definition(for: task.id)?.category {
|
switch PillCatalog.definition(for: task.id)?.category {
|
||||||
case .workspace, .agent: section = "agents"
|
case .workspace, .agent: section = "agents"
|
||||||
@@ -2056,7 +2056,7 @@ struct VercelDeploymentListView: View {
|
|||||||
Text("Vercel")
|
Text("Vercel")
|
||||||
.font(.system(size: 12, weight: .semibold))
|
.font(.system(size: 12, weight: .semibold))
|
||||||
.foregroundColor(Color(hex: "#F5F6F8"))
|
.foregroundColor(Color(hex: "#F5F6F8"))
|
||||||
Text("Deployments")
|
Text("Деплои")
|
||||||
.font(.system(size: 11))
|
.font(.system(size: 11))
|
||||||
.foregroundColor(Color(hex: "#8E939C"))
|
.foregroundColor(Color(hex: "#8E939C"))
|
||||||
}
|
}
|
||||||
@@ -2171,7 +2171,7 @@ struct VercelDetailView: View {
|
|||||||
.font(.system(size: 10))
|
.font(.system(size: 10))
|
||||||
.foregroundColor(Color(hex: "#6B7079"))
|
.foregroundColor(Color(hex: "#6B7079"))
|
||||||
}
|
}
|
||||||
Text(deployment.timeAgo + " ago")
|
Text(deployment.timeAgo == "только что" ? deployment.timeAgo : deployment.timeAgo + " назад")
|
||||||
.font(.system(size: 10))
|
.font(.system(size: 10))
|
||||||
.foregroundColor(Color(hex: "#6B7079"))
|
.foregroundColor(Color(hex: "#6B7079"))
|
||||||
}
|
}
|
||||||
@@ -2225,7 +2225,7 @@ struct ResendCardView: View {
|
|||||||
Text("Resend")
|
Text("Resend")
|
||||||
.font(.system(size: 12, weight: .semibold))
|
.font(.system(size: 12, weight: .semibold))
|
||||||
.foregroundColor(Color(hex: "#F5F6F8"))
|
.foregroundColor(Color(hex: "#F5F6F8"))
|
||||||
Text("Emails")
|
Text("Письма")
|
||||||
.font(.system(size: 11))
|
.font(.system(size: 11))
|
||||||
.foregroundColor(Color(hex: "#8E939C"))
|
.foregroundColor(Color(hex: "#8E939C"))
|
||||||
if let total {
|
if let total {
|
||||||
@@ -2367,7 +2367,7 @@ struct GitHubPulseCardView: View {
|
|||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
} else {
|
} else {
|
||||||
Text("Overview")
|
Text("Обзор")
|
||||||
.font(.system(size: 11))
|
.font(.system(size: 11))
|
||||||
.foregroundColor(Color(hex: "#8E939C"))
|
.foregroundColor(Color(hex: "#8E939C"))
|
||||||
}
|
}
|
||||||
@@ -2385,13 +2385,13 @@ struct GitHubPulseCardView: View {
|
|||||||
if n == 0 { return "0" }
|
if n == 0 { return "0" }
|
||||||
let failing = pulse.myPRs.filter { $0.ci == .failure }.count
|
let failing = pulse.myPRs.filter { $0.ci == .failure }.count
|
||||||
let pending = pulse.myPRs.filter { $0.ci == .pending }.count
|
let pending = pulse.myPRs.filter { $0.ci == .pending }.count
|
||||||
if failing > 0 { return "\(n) · \(failing) failing" }
|
if failing > 0 { return "\(n) · \(failing) с ошибкой" }
|
||||||
if pending > 0 { return "\(n) · running" }
|
if pending > 0 { return "\(n) · идёт" }
|
||||||
return "\(n)"
|
return "\(n)"
|
||||||
}()
|
}()
|
||||||
GitHubStatRow(
|
GitHubStatRow(
|
||||||
icon: "arrow.triangle.pull", iconColor: ciColor(prWorst),
|
icon: "arrow.triangle.pull", iconColor: ciColor(prWorst),
|
||||||
label: "My PRs", value: prValue
|
label: "Мои PR", value: prValue
|
||||||
) { onTapSection(.myPRs) }
|
) { onTapSection(.myPRs) }
|
||||||
|
|
||||||
// To review
|
// To review
|
||||||
@@ -2399,7 +2399,7 @@ struct GitHubPulseCardView: View {
|
|||||||
GitHubStatRow(
|
GitHubStatRow(
|
||||||
icon: "eye",
|
icon: "eye",
|
||||||
iconColor: reviewCount > 0 ? "#8AB4F8" : "#6B7079",
|
iconColor: reviewCount > 0 ? "#8AB4F8" : "#6B7079",
|
||||||
label: "To review",
|
label: "На ревью",
|
||||||
value: "\(reviewCount)"
|
value: "\(reviewCount)"
|
||||||
) { onTapSection(.toReview) }
|
) { onTapSection(.toReview) }
|
||||||
|
|
||||||
@@ -2409,18 +2409,18 @@ struct GitHubPulseCardView: View {
|
|||||||
switch mainWorst {
|
switch mainWorst {
|
||||||
case .failure:
|
case .failure:
|
||||||
let n = pulse.mainCI.filter { $0.ci == .failure }.count
|
let n = pulse.mainCI.filter { $0.ci == .failure }.count
|
||||||
return ("xmark.octagon.fill", "#F4505E", "\(n) failing")
|
return ("xmark.octagon.fill", "#F4505E", "\(n) с ошибкой")
|
||||||
case .pending:
|
case .pending:
|
||||||
return ("checkmark.seal.fill", "#F5A524", "running")
|
return ("checkmark.seal.fill", "#F5A524", "идёт")
|
||||||
case .success:
|
case .success:
|
||||||
return ("checkmark.seal.fill", "#22C55E", "all green")
|
return ("checkmark.seal.fill", "#22C55E", "всё зелёное")
|
||||||
case .unknown:
|
case .unknown:
|
||||||
return ("checkmark.seal.fill", "#6B7079", pulse.mainCI.isEmpty ? "no repos" : "unknown")
|
return ("checkmark.seal.fill", "#6B7079", pulse.mainCI.isEmpty ? "нет репо" : "неизвестно")
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
GitHubStatRow(
|
GitHubStatRow(
|
||||||
icon: ciIcon, iconColor: ciIconColor,
|
icon: ciIcon, iconColor: ciIconColor,
|
||||||
label: "Default branch CI", value: ciValue
|
label: "CI основной ветки", value: ciValue
|
||||||
) { onTapSection(.mainCI) }
|
) { onTapSection(.mainCI) }
|
||||||
}
|
}
|
||||||
.padding(.top, 6)
|
.padding(.top, 6)
|
||||||
@@ -2479,10 +2479,10 @@ struct GitHubDetailView: View {
|
|||||||
|
|
||||||
private var title: String {
|
private var title: String {
|
||||||
switch section {
|
switch section {
|
||||||
case .myPRs: return "My PRs"
|
case .myPRs: return "Мои PR"
|
||||||
case .toReview: return "To review"
|
case .toReview: return "На ревью"
|
||||||
case .mainCI: return "Default branch CI"
|
case .mainCI: return "CI основной ветки"
|
||||||
case .activity: return "Activity"
|
case .activity: return "Активность"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2530,7 +2530,7 @@ struct GitHubDetailView: View {
|
|||||||
|
|
||||||
// List
|
// List
|
||||||
if items.isEmpty && repoItems.isEmpty {
|
if items.isEmpty && repoItems.isEmpty {
|
||||||
Text("Nothing here")
|
Text("Пусто")
|
||||||
.font(.system(size: 10.5))
|
.font(.system(size: 10.5))
|
||||||
.foregroundColor(Color(hex: "#6B7079"))
|
.foregroundColor(Color(hex: "#6B7079"))
|
||||||
.padding(.top, 8)
|
.padding(.top, 8)
|
||||||
@@ -2619,16 +2619,15 @@ private struct GitHubActivityDetailContent: View {
|
|||||||
if let day = hoveredDay {
|
if let day = hoveredDay {
|
||||||
let label: String
|
let label: String
|
||||||
switch day.count {
|
switch day.count {
|
||||||
case 0: label = "No contributions"
|
case 0: label = "Нет вкладов"
|
||||||
case 1: label = "1 contribution"
|
default: label = "\(day.count) \(ruPlural(day.count, "вклад", "вклада", "вкладов"))"
|
||||||
default: label = "\(day.count) contributions"
|
|
||||||
}
|
}
|
||||||
return "\(activityDateLabel(day.date)) · \(label)"
|
return "\(activityDateLabel(day.date)) · \(label)"
|
||||||
}
|
}
|
||||||
guard let act = activity else { return "" }
|
guard let act = activity else { return "" }
|
||||||
let total = activityTotalLabel(act.total)
|
let total = activityTotalLabel(act.total)
|
||||||
if let s = stats { return "\(total) past year · \(s.totalRepos) repos" }
|
if let s = stats { return "\(total) за год · \(s.totalRepos) репо" }
|
||||||
return "\(total) past year"
|
return "\(total) за год"
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -2639,7 +2638,7 @@ private struct GitHubActivityDetailContent: View {
|
|||||||
HStack(spacing: 3) {
|
HStack(spacing: 3) {
|
||||||
Image(systemName: "chevron.left")
|
Image(systemName: "chevron.left")
|
||||||
.font(.system(size: 8, weight: .medium))
|
.font(.system(size: 8, weight: .medium))
|
||||||
Text("Activity")
|
Text("Активность")
|
||||||
.font(.system(size: 12, weight: .semibold))
|
.font(.system(size: 12, weight: .semibold))
|
||||||
}
|
}
|
||||||
.foregroundColor(Color(hex: "#F5F6F8"))
|
.foregroundColor(Color(hex: "#F5F6F8"))
|
||||||
@@ -2691,7 +2690,7 @@ private struct GitHubActivityDetailContent: View {
|
|||||||
.padding(.leading, 108)
|
.padding(.leading, 108)
|
||||||
.padding(.trailing, 12)
|
.padding(.trailing, 12)
|
||||||
} else {
|
} else {
|
||||||
Text("Loading…")
|
Text("Загрузка…")
|
||||||
.font(.system(size: 10.5))
|
.font(.system(size: 10.5))
|
||||||
.foregroundColor(Color(hex: "#6B7079"))
|
.foregroundColor(Color(hex: "#6B7079"))
|
||||||
.padding(.top, 8)
|
.padding(.top, 8)
|
||||||
@@ -2710,14 +2709,14 @@ private struct GitHubActivityDetailContent: View {
|
|||||||
guard parts.count == 3,
|
guard parts.count == 3,
|
||||||
let month = Int(parts[1]), month >= 1 && month <= 12,
|
let month = Int(parts[1]), month >= 1 && month <= 12,
|
||||||
let day = Int(parts[2]) else { return dateStr }
|
let day = Int(parts[2]) else { return dateStr }
|
||||||
let months = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]
|
let months = ["янв","фев","мар","апр","мая","июн","июл","авг","сен","окт","ноя","дек"]
|
||||||
return "\(months[month - 1]) \(day)"
|
return "\(day) \(months[month - 1])"
|
||||||
}
|
}
|
||||||
|
|
||||||
private func activityTotalLabel(_ n: Int) -> String {
|
private func activityTotalLabel(_ n: Int) -> String {
|
||||||
let nf = NumberFormatter()
|
let nf = NumberFormatter()
|
||||||
nf.numberStyle = .decimal
|
nf.numberStyle = .decimal
|
||||||
nf.locale = Locale(identifier: "en_US")
|
nf.locale = Locale(identifier: "ru_RU")
|
||||||
return nf.string(from: NSNumber(value: n)) ?? "\(n)"
|
return nf.string(from: NSNumber(value: n)) ?? "\(n)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2771,7 +2770,7 @@ private struct GitHubPRRowView: View {
|
|||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.truncationMode(.tail)
|
.truncationMode(.tail)
|
||||||
if pr.isDraft {
|
if pr.isDraft {
|
||||||
Text("Draft")
|
Text("Черновик")
|
||||||
.font(.system(size: 9.5))
|
.font(.system(size: 9.5))
|
||||||
.foregroundColor(Color(hex: "#6B7079"))
|
.foregroundColor(Color(hex: "#6B7079"))
|
||||||
}
|
}
|
||||||
@@ -2857,7 +2856,7 @@ struct GitHubStatsCardView: View {
|
|||||||
Text("GitHub")
|
Text("GitHub")
|
||||||
.font(.system(size: 12, weight: .semibold))
|
.font(.system(size: 12, weight: .semibold))
|
||||||
.foregroundColor(Color(hex: "#F5F6F8"))
|
.foregroundColor(Color(hex: "#F5F6F8"))
|
||||||
Text("Overview")
|
Text("Обзор")
|
||||||
.font(.system(size: 11))
|
.font(.system(size: 11))
|
||||||
.foregroundColor(Color(hex: "#8E939C"))
|
.foregroundColor(Color(hex: "#8E939C"))
|
||||||
}
|
}
|
||||||
@@ -2868,9 +2867,9 @@ struct GitHubStatsCardView: View {
|
|||||||
// Stats rows
|
// Stats rows
|
||||||
VStack(alignment: .leading, spacing: 5) {
|
VStack(alignment: .leading, spacing: 5) {
|
||||||
StatRow(icon: "star.fill", color: "#F5A524",
|
StatRow(icon: "star.fill", color: "#F5A524",
|
||||||
label: "Total stars", value: formatCount(stats.totalStars))
|
label: "Всего звёзд", value: formatCount(stats.totalStars))
|
||||||
StatRow(icon: "square.stack.fill", color: "#6B7079",
|
StatRow(icon: "square.stack.fill", color: "#6B7079",
|
||||||
label: "Repositories", value: "\(stats.totalRepos)")
|
label: "Репозитории", value: "\(stats.totalRepos)")
|
||||||
}
|
}
|
||||||
.padding(.top, 8)
|
.padding(.top, 8)
|
||||||
.padding(.leading, 108)
|
.padding(.leading, 108)
|
||||||
@@ -2926,7 +2925,7 @@ struct StripeCardView: View {
|
|||||||
Text("Stripe")
|
Text("Stripe")
|
||||||
.font(.system(size: 12, weight: .semibold))
|
.font(.system(size: 12, weight: .semibold))
|
||||||
.foregroundColor(Color(hex: "#F5F6F8"))
|
.foregroundColor(Color(hex: "#F5F6F8"))
|
||||||
Text("Payments")
|
Text("Платежи")
|
||||||
.font(.system(size: 11))
|
.font(.system(size: 11))
|
||||||
.foregroundColor(Color(hex: "#8E939C"))
|
.foregroundColor(Color(hex: "#8E939C"))
|
||||||
}
|
}
|
||||||
@@ -2981,7 +2980,7 @@ private struct StripePaymentRow: View {
|
|||||||
let accent = payment.isSuccess ? Color(hex: "#22C55E") : Color(hex: "#F4505E")
|
let accent = payment.isSuccess ? Color(hex: "#22C55E") : Color(hex: "#F4505E")
|
||||||
HStack(spacing: 5) {
|
HStack(spacing: 5) {
|
||||||
Circle().fill(accent).frame(width: 5, height: 5)
|
Circle().fill(accent).frame(width: 5, height: 5)
|
||||||
Text(payment.description ?? "Payment")
|
Text(payment.description ?? "Платёж")
|
||||||
.font(.system(size: 11))
|
.font(.system(size: 11))
|
||||||
.foregroundColor(Color(hex: "#C5C8CD"))
|
.foregroundColor(Color(hex: "#C5C8CD"))
|
||||||
.lineLimit(1).truncationMode(.tail)
|
.lineLimit(1).truncationMode(.tail)
|
||||||
@@ -3053,7 +3052,7 @@ struct CalcomCalendarView: View {
|
|||||||
private let cal = Calendar.current
|
private let cal = Calendar.current
|
||||||
|
|
||||||
private var navLabel: String {
|
private var navLabel: String {
|
||||||
let f = DateFormatter(); f.dateFormat = "MMMM yyyy"
|
let f = DateFormatter(); f.locale = Locale(identifier: "ru_RU"); f.dateFormat = "LLLL yyyy"
|
||||||
return "\(f.string(from: displayMonth)) Q\(displayHalf)"
|
return "\(f.string(from: displayMonth)) Q\(displayHalf)"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3111,7 +3110,7 @@ struct CalcomCalendarView: View {
|
|||||||
HStack(spacing: 6) {
|
HStack(spacing: 6) {
|
||||||
Circle().fill(Color(hex: "#C9956A")).frame(width: 7, height: 7)
|
Circle().fill(Color(hex: "#C9956A")).frame(width: 7, height: 7)
|
||||||
Text("Cal.com").font(.system(size: 12, weight: .semibold)).foregroundColor(Color(hex: "#F5F6F8"))
|
Text("Cal.com").font(.system(size: 12, weight: .semibold)).foregroundColor(Color(hex: "#F5F6F8"))
|
||||||
Text("Schedule").font(.system(size: 11)).foregroundColor(Color(hex: "#8E939C"))
|
Text("Расписание").font(.system(size: 11)).foregroundColor(Color(hex: "#8E939C"))
|
||||||
}
|
}
|
||||||
.padding(.top, 6).padding(.leading, 108).padding(.trailing, 36)
|
.padding(.top, 6).padding(.leading, 108).padding(.trailing, 36)
|
||||||
|
|
||||||
@@ -3150,7 +3149,7 @@ private struct CalcomWeekRow: View {
|
|||||||
|
|
||||||
private var weekLabel: String {
|
private var weekLabel: String {
|
||||||
guard let first = week.compactMap({ $0 }).first else { return "" }
|
guard let first = week.compactMap({ $0 }).first else { return "" }
|
||||||
let f = DateFormatter(); f.dateFormat = "dd/MM"
|
let f = DateFormatter(); f.dateFormat = "dd.MM"
|
||||||
return f.string(from: first)
|
return f.string(from: first)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3207,7 +3206,7 @@ struct CalcomDayView: View {
|
|||||||
.padding(.top, 6).padding(.trailing, 12)
|
.padding(.top, 6).padding(.trailing, 12)
|
||||||
|
|
||||||
if bookings.isEmpty {
|
if bookings.isEmpty {
|
||||||
Text("No calls scheduled").font(.system(size: 11)).foregroundColor(Color(hex: "#6B7079"))
|
Text("Звонков нет").font(.system(size: 11)).foregroundColor(Color(hex: "#6B7079"))
|
||||||
.padding(.leading, 116).padding(.top, 8)
|
.padding(.leading, 116).padding(.top, 8)
|
||||||
} else {
|
} else {
|
||||||
VStack(alignment: .leading, spacing: 3) {
|
VStack(alignment: .leading, spacing: 3) {
|
||||||
@@ -3237,7 +3236,7 @@ struct CalcomDayView: View {
|
|||||||
.transition(.opacity)
|
.transition(.opacity)
|
||||||
}
|
}
|
||||||
private var dayLabel: String {
|
private var dayLabel: String {
|
||||||
let f = DateFormatter(); f.dateFormat = "EEEE d MMMM"; return f.string(from: date)
|
let f = DateFormatter(); f.locale = Locale(identifier: "ru_RU"); f.dateFormat = "EEEE, d MMMM"; return f.string(from: date)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3304,7 +3303,7 @@ struct NotionCardView: View {
|
|||||||
HStack(spacing: 6) {
|
HStack(spacing: 6) {
|
||||||
Circle().fill(Color(hex: "#E8E8E8")).frame(width: 7, height: 7)
|
Circle().fill(Color(hex: "#E8E8E8")).frame(width: 7, height: 7)
|
||||||
Text("Notion").font(.system(size: 12, weight: .semibold)).foregroundColor(Color(hex: "#F5F6F8"))
|
Text("Notion").font(.system(size: 12, weight: .semibold)).foregroundColor(Color(hex: "#F5F6F8"))
|
||||||
Text("Recent").font(.system(size: 11)).foregroundColor(Color(hex: "#8E939C"))
|
Text("Недавние").font(.system(size: 11)).foregroundColor(Color(hex: "#8E939C"))
|
||||||
}
|
}
|
||||||
.padding(.top, 6).padding(.leading, 108).padding(.trailing, 36)
|
.padding(.top, 6).padding(.leading, 108).padding(.trailing, 36)
|
||||||
|
|
||||||
@@ -3348,7 +3347,7 @@ struct N8nDetailView: View {
|
|||||||
|
|
||||||
private var success: Bool { task.state == .finished }
|
private var success: Bool { task.state == .finished }
|
||||||
private var accent: Color { success ? Color(hex: "#22C55E") : Color(hex: "#F4505E") }
|
private var accent: Color { success ? Color(hex: "#22C55E") : Color(hex: "#F4505E") }
|
||||||
private var statusLabel: String { success ? "Success" : "Failed" }
|
private var statusLabel: String { success ? "Успех" : "Ошибка" }
|
||||||
private var detail: String? { task.steps.dropFirst().first }
|
private var detail: String? { task.steps.dropFirst().first }
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -3367,7 +3366,7 @@ struct N8nDetailView: View {
|
|||||||
|
|
||||||
Circle().fill(accent).frame(width: 6, height: 6)
|
Circle().fill(accent).frame(width: 6, height: 6)
|
||||||
|
|
||||||
Text(task.steps.first ?? "Workflow")
|
Text(task.steps.first ?? "Сценарий")
|
||||||
.font(.system(size: 11, weight: .semibold))
|
.font(.system(size: 11, weight: .semibold))
|
||||||
.foregroundColor(Color(hex: "#F5F6F8"))
|
.foregroundColor(Color(hex: "#F5F6F8"))
|
||||||
.lineLimit(1).truncationMode(.middle)
|
.lineLimit(1).truncationMode(.middle)
|
||||||
@@ -3395,7 +3394,7 @@ struct N8nDetailView: View {
|
|||||||
}
|
}
|
||||||
.frame(maxHeight: 88)
|
.frame(maxHeight: 88)
|
||||||
} else {
|
} else {
|
||||||
Text(success ? "Completed successfully." : "No error details available.")
|
Text(success ? "Успешно завершено." : "Нет подробностей об ошибке.")
|
||||||
.font(.system(size: 11))
|
.font(.system(size: 11))
|
||||||
.foregroundColor(Color(hex: "#6B7079"))
|
.foregroundColor(Color(hex: "#6B7079"))
|
||||||
}
|
}
|
||||||
@@ -3891,13 +3890,13 @@ struct MusicCardView: View {
|
|||||||
.padding(.leading, 108)
|
.padding(.leading, 108)
|
||||||
.padding(.trailing, 36)
|
.padding(.trailing, 36)
|
||||||
|
|
||||||
Text("Allow BroV to control Music")
|
Text("Разреши BroV управлять Музыкой")
|
||||||
.font(.system(size: 11))
|
.font(.system(size: 11))
|
||||||
.foregroundColor(Color(hex: "#8E939C"))
|
.foregroundColor(Color(hex: "#8E939C"))
|
||||||
.padding(.leading, 108)
|
.padding(.leading, 108)
|
||||||
.padding(.trailing, 12)
|
.padding(.trailing, 12)
|
||||||
|
|
||||||
Button("Open Settings…") { MusicController.shared.openAutomationSettings() }
|
Button("Открыть настройки…") { MusicController.shared.openAutomationSettings() }
|
||||||
.font(.system(size: 11, weight: .medium))
|
.font(.system(size: 11, weight: .medium))
|
||||||
.foregroundColor(Color(hex: "#FA2D48").opacity(0.85))
|
.foregroundColor(Color(hex: "#FA2D48").opacity(0.85))
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
@@ -4032,8 +4031,8 @@ struct WardrobeView: View {
|
|||||||
if let h = hoveredOutfit {
|
if let h = hoveredOutfit {
|
||||||
if h == .auto {
|
if h == .auto {
|
||||||
let seasonal = Outfit.seasonal(for: Date(), calendar: .current)
|
let seasonal = Outfit.seasonal(for: Date(), calendar: .current)
|
||||||
let name = seasonal == .none ? "None" : seasonal.displayName
|
let name = seasonal == .none ? "Нет" : seasonal.displayName
|
||||||
return "Auto · follows the seasons (now: \(name))"
|
return "Авто · по сезону (сейчас: \(name))"
|
||||||
}
|
}
|
||||||
return h.displayName
|
return h.displayName
|
||||||
}
|
}
|
||||||
@@ -4041,8 +4040,8 @@ struct WardrobeView: View {
|
|||||||
let sel = state.mochiOutfitSelection
|
let sel = state.mochiOutfitSelection
|
||||||
if sel == .auto {
|
if sel == .auto {
|
||||||
let seasonal = Outfit.seasonal(for: Date(), calendar: .current)
|
let seasonal = Outfit.seasonal(for: Date(), calendar: .current)
|
||||||
let name = seasonal == .none ? "None" : seasonal.displayName
|
let name = seasonal == .none ? "Нет" : seasonal.displayName
|
||||||
return "Auto · \(name)"
|
return "Авто · \(name)"
|
||||||
}
|
}
|
||||||
return sel.displayName
|
return sel.displayName
|
||||||
}
|
}
|
||||||
@@ -4051,7 +4050,7 @@ struct WardrobeView: View {
|
|||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
// Header
|
// Header
|
||||||
HStack(spacing: 6) {
|
HStack(spacing: 6) {
|
||||||
Text("Wardrobe")
|
Text("Гардероб")
|
||||||
.font(.system(size: 12, weight: .semibold))
|
.font(.system(size: 12, weight: .semibold))
|
||||||
.foregroundColor(Color(hex: "#F5F6F8"))
|
.foregroundColor(Color(hex: "#F5F6F8"))
|
||||||
Spacer(minLength: 4)
|
Spacer(minLength: 4)
|
||||||
@@ -4211,7 +4210,7 @@ private func drawOutfitIcon(context: GraphicsContext, size: CGSize, outfit: Outf
|
|||||||
badge.addRoundedRect(in: CGRect(x: -bw/2, y: -bh/2, width: bw, height: bh),
|
badge.addRoundedRect(in: CGRect(x: -bw/2, y: -bh/2, width: bw, height: bh),
|
||||||
cornerSize: CGSize(width: bh/2, height: bh/2))
|
cornerSize: CGSize(width: bh/2, height: bh/2))
|
||||||
badgeCtx.fill(badge, with: .color(Color.black.opacity(0.60)))
|
badgeCtx.fill(badge, with: .color(Color.black.opacity(0.60)))
|
||||||
badgeCtx.draw(Text("AUTO").font(.system(size: 4.2, weight: .semibold)).foregroundColor(.white),
|
badgeCtx.draw(Text("АВТО").font(.system(size: 4.2, weight: .semibold)).foregroundColor(.white),
|
||||||
at: .zero)
|
at: .zero)
|
||||||
|
|
||||||
case .none:
|
case .none:
|
||||||
@@ -4620,7 +4619,7 @@ struct SettingsIslandView: View {
|
|||||||
.labelsHidden()
|
.labelsHidden()
|
||||||
.scaleEffect(0.75)
|
.scaleEffect(0.75)
|
||||||
.frame(width: 44)
|
.frame(width: 44)
|
||||||
Text("Sound")
|
Text("Звук")
|
||||||
.font(.system(size: 12.5))
|
.font(.system(size: 12.5))
|
||||||
.foregroundColor(Color(hex: "#C5C8CD"))
|
.foregroundColor(Color(hex: "#C5C8CD"))
|
||||||
Slider(value: $state.soundVolume, in: 0...0.2)
|
Slider(value: $state.soundVolume, in: 0...0.2)
|
||||||
@@ -4634,13 +4633,13 @@ struct SettingsIslandView: View {
|
|||||||
.font(.system(size: 12))
|
.font(.system(size: 12))
|
||||||
.foregroundColor(Color(hex: "#8E939C"))
|
.foregroundColor(Color(hex: "#8E939C"))
|
||||||
.frame(width: 16)
|
.frame(width: 16)
|
||||||
Text("Auto-close · \(Int(state.autoCloseInterval))s")
|
Text("Автозакрытие · \(Int(state.autoCloseInterval)) с")
|
||||||
.font(.system(size: 12))
|
.font(.system(size: 12))
|
||||||
.foregroundColor(Color(hex: "#C5C8CD"))
|
.foregroundColor(Color(hex: "#C5C8CD"))
|
||||||
Spacer()
|
Spacer()
|
||||||
HStack(spacing: 6) {
|
HStack(spacing: 6) {
|
||||||
ForEach([10, 15, 30], id: \.self) { s in
|
ForEach([10, 15, 30], id: \.self) { s in
|
||||||
Button("\(s)s") {
|
Button("\(s) с") {
|
||||||
state.autoCloseInterval = Double(s)
|
state.autoCloseInterval = Double(s)
|
||||||
}
|
}
|
||||||
.font(.system(size: 11))
|
.font(.system(size: 11))
|
||||||
@@ -4658,7 +4657,7 @@ struct SettingsIslandView: View {
|
|||||||
StatusBadge(label: "Claude Code", ok: claudeConnected)
|
StatusBadge(label: "Claude Code", ok: claudeConnected)
|
||||||
StatusBadge(label: "API", ok: apiConnected)
|
StatusBadge(label: "API", ok: apiConnected)
|
||||||
Spacer()
|
Spacer()
|
||||||
Button("Settings…") {
|
Button("Настройки…") {
|
||||||
NotificationCenter.default.post(name: .openFullSettings, object: nil)
|
NotificationCenter.default.post(name: .openFullSettings, object: nil)
|
||||||
}
|
}
|
||||||
.font(.system(size: 11.5))
|
.font(.system(size: 11.5))
|
||||||
@@ -4718,3 +4717,11 @@ extension Color {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Russian plural form picker: 1 вклад, 2 вклада, 5 вкладов.
|
||||||
|
fileprivate func ruPlural(_ n: Int, _ one: String, _ few: String, _ many: String) -> String {
|
||||||
|
let n10 = abs(n) % 10, n100 = abs(n) % 100
|
||||||
|
if n10 == 1 && n100 != 11 { return one }
|
||||||
|
if (2...4).contains(n10) && !(12...14).contains(n100) { return few }
|
||||||
|
return many
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ enum LocalChatError: Error {
|
|||||||
|
|
||||||
var localizedDescription: String {
|
var localizedDescription: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .serverUnreachable(let url): return "Cannot reach \(url). Is the server running?"
|
case .serverUnreachable(let url): return "Нет связи с \(url). Сервер запущен?"
|
||||||
case .modelNotFound(let model): return "Model '\(model)' is not installed."
|
case .modelNotFound(let model): return "Модель «\(model)» не установлена."
|
||||||
case .serverError(let msg): return msg
|
case .serverError(let msg): return msg
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ final class N8nPoller: @unchecked Sendable {
|
|||||||
let items = main.first else { return nil }
|
let items = main.first else { return nil }
|
||||||
|
|
||||||
let count = items.count
|
let count = items.count
|
||||||
let header = "→ \(lastNode) · \(count) item\(count == 1 ? "" : "s")"
|
let header = "→ \(lastNode) · \(count) \(ruItems(count))"
|
||||||
|
|
||||||
// Preview first item's JSON keys (up to 4)
|
// Preview first item's JSON keys (up to 4)
|
||||||
if let firstItem = items.first,
|
if let firstItem = items.first,
|
||||||
@@ -257,3 +257,11 @@ final class N8nPoller: @unchecked Sendable {
|
|||||||
appendAppLog("n8n.log", message, timestampFormat: "HH:mm:ss")
|
appendAppLog("n8n.log", message, timestampFormat: "HH:mm:ss")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Russian plural for "элемент": 1 элемент, 2 элемента, 5 элементов.
|
||||||
|
fileprivate func ruItems(_ n: Int) -> String {
|
||||||
|
let mod10 = n % 10, mod100 = n % 100
|
||||||
|
if mod10 == 1 && mod100 != 11 { return "элемент" }
|
||||||
|
if (2...4).contains(mod10) && !(12...14).contains(mod100) { return "элемента" }
|
||||||
|
return "элементов"
|
||||||
|
}
|
||||||
|
|||||||
@@ -35,9 +35,9 @@ final class NotionPoller: @unchecked Sendable {
|
|||||||
let code = (response as? HTTPURLResponse)?.statusCode ?? 0
|
let code = (response as? HTTPURLResponse)?.statusCode ?? 0
|
||||||
if code != 200 {
|
if code != 200 {
|
||||||
let msg: String
|
let msg: String
|
||||||
if code == 401 { msg = "Invalid API key (401)" }
|
if code == 401 { msg = "Неверный API-ключ (401)" }
|
||||||
else if code == 0 { msg = error?.localizedDescription ?? "No connection" }
|
else if code == 0 { msg = error?.localizedDescription ?? "Нет соединения" }
|
||||||
else { msg = "API error \(code)" }
|
else { msg = "Ошибка API \(code)" }
|
||||||
DispatchQueue.main.async { AppState.shared.notionError = msg }
|
DispatchQueue.main.async { AppState.shared.notionError = msg }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -58,7 +58,7 @@ final class NotionPoller: @unchecked Sendable {
|
|||||||
guard let id = obj["id"] as? String else { return nil }
|
guard let id = obj["id"] as? String else { return nil }
|
||||||
let objType = obj["object"] as? String ?? "page"
|
let objType = obj["object"] as? String ?? "page"
|
||||||
|
|
||||||
var title = "Untitled"
|
var title = "Без названия"
|
||||||
if objType == "database" {
|
if objType == "database" {
|
||||||
if let arr = obj["title"] as? [[String: Any]],
|
if let arr = obj["title"] as? [[String: Any]],
|
||||||
let text = arr.first?["plain_text"] as? String, !text.isEmpty {
|
let text = arr.first?["plain_text"] as? String, !text.isEmpty {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -38,16 +38,16 @@ enum ShortcutAction: String, CaseIterable, Sendable {
|
|||||||
|
|
||||||
var displayName: String {
|
var displayName: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .toggleIsland: return "Open / close island"
|
case .toggleIsland: return "Открыть / закрыть остров"
|
||||||
case .openChat: return "Open chat"
|
case .openChat: return "Открыть чат"
|
||||||
case .goToAlert: return "Go to alert"
|
case .goToAlert: return "Перейти к уведомлению"
|
||||||
case .jumpToTerminal: return "Jump to terminal"
|
case .jumpToTerminal: return "Перейти в терминал"
|
||||||
case .attachFrontWindow: return "Attach front window"
|
case .attachFrontWindow: return "Прикрепить активное окно"
|
||||||
case .nextPill: return "Next pill"
|
case .nextPill: return "Следующая плашка"
|
||||||
case .prevPill: return "Previous pill"
|
case .prevPill: return "Предыдущая плашка"
|
||||||
case .muteToggle: return "Mute / unmute sounds"
|
case .muteToggle: return "Выключить / включить звук"
|
||||||
case .desktopToggle: return "BroV on / off desktop"
|
case .desktopToggle: return "BroV на рабочий стол / обратно"
|
||||||
case .wardrobeToggle: return "Open / close wardrobe"
|
case .wardrobeToggle: return "Открыть / закрыть гардероб"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,15 +222,15 @@ enum ShortcutLogic {
|
|||||||
|
|
||||||
/// Descriptive table of island-local shortcuts for the Settings view.
|
/// Descriptive table of island-local shortcuts for the Settings view.
|
||||||
static let islandShortcuts: [(key: String, description: String)] = [
|
static let islandShortcuts: [(key: String, description: String)] = [
|
||||||
("⌘→ / ⌘←", "Next / previous pill"),
|
("⌘→ / ⌘←", "Следующая / предыдущая плашка"),
|
||||||
("⌘1 – ⌘9", "Switch to pill by number"),
|
("⌘1 – ⌘9", "Плашка по номеру"),
|
||||||
("⌘↓ / ⌘↑", "Navigate list items"),
|
("⌘↓ / ⌘↑", "Перемещение по списку"),
|
||||||
("⌘O", "Open selected item"),
|
("⌘O", "Открыть выбранный элемент"),
|
||||||
("⌘E", "Open / close current diff"),
|
("⌘E", "Открыть / закрыть текущий diff"),
|
||||||
("⌘↩", "Send chat message"),
|
("⌘↩", "Отправить сообщение в чат"),
|
||||||
("⌘K", "New conversation"),
|
("⌘K", "Новый диалог"),
|
||||||
("⌘,", "Open Settings"),
|
("⌘,", "Открыть настройки"),
|
||||||
("⌘P", "Pin / unpin island"),
|
("⌘P", "Закрепить / открепить остров"),
|
||||||
("⎋", "Close island (if not pinned)"),
|
("⎋", "Закрыть остров (если не закреплён)"),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ struct ShortcutsSettingsView: View {
|
|||||||
// MARK: - Global shortcuts
|
// MARK: - Global shortcuts
|
||||||
|
|
||||||
private var globalSection: some View {
|
private var globalSection: some View {
|
||||||
GroupBox("Global shortcuts — work from any app") {
|
GroupBox("Глобальные сочетания — работают в любом приложении") {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
ForEach(globalActions, id: \.self) { action in
|
ForEach(globalActions, id: \.self) { action in
|
||||||
shortcutRow(action)
|
shortcutRow(action)
|
||||||
@@ -36,7 +36,7 @@ struct ShortcutsSettingsView: View {
|
|||||||
Divider().padding(.top, 8)
|
Divider().padding(.top, 8)
|
||||||
HStack {
|
HStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
Button("Reset all to defaults") { resetAll() }
|
Button("Сбросить всё по умолчанию") { resetAll() }
|
||||||
.buttonStyle(.bordered)
|
.buttonStyle(.bordered)
|
||||||
.controlSize(.small)
|
.controlSize(.small)
|
||||||
}
|
}
|
||||||
@@ -82,9 +82,9 @@ struct ShortcutsSettingsView: View {
|
|||||||
.onChange(of: specs[action]) { _, _ in specChanged(action) }
|
.onChange(of: specs[action]) { _, _ in specChanged(action) }
|
||||||
|
|
||||||
if hasSysConf {
|
if hasSysConf {
|
||||||
conflictTag("System conflict", color: .orange)
|
conflictTag("Конфликт с системой", color: .orange)
|
||||||
} else if hasIntConf {
|
} else if hasIntConf {
|
||||||
conflictTag("Duplicate", color: .red)
|
conflictTag("Повтор", color: .red)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.vertical, 5)
|
.padding(.vertical, 5)
|
||||||
@@ -104,7 +104,7 @@ struct ShortcutsSettingsView: View {
|
|||||||
// MARK: - Island-local shortcuts (read-only)
|
// MARK: - Island-local shortcuts (read-only)
|
||||||
|
|
||||||
private var islandSection: some View {
|
private var islandSection: some View {
|
||||||
GroupBox("Island shortcuts — active when the island is focused") {
|
GroupBox("Сочетания острова — работают, когда остров в фокусе") {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
ForEach(Array(ShortcutLogic.islandShortcuts.enumerated()), id: \.offset) { _, pair in
|
ForEach(Array(ShortcutLogic.islandShortcuts.enumerated()), id: \.offset) { _, pair in
|
||||||
HStack(spacing: 10) {
|
HStack(spacing: 10) {
|
||||||
|
|||||||
@@ -44,10 +44,10 @@ final class StripePoller: @unchecked Sendable {
|
|||||||
let code = (response as? HTTPURLResponse)?.statusCode ?? 0
|
let code = (response as? HTTPURLResponse)?.statusCode ?? 0
|
||||||
if code != 200 {
|
if code != 200 {
|
||||||
let errMsg: String
|
let errMsg: String
|
||||||
if code == 401 { errMsg = "Invalid API key (401)" }
|
if code == 401 { errMsg = "Неверный API-ключ (401)" }
|
||||||
else if code == 403 { errMsg = "Use secret key (sk_live_… not pk_live_…)" }
|
else if code == 403 { errMsg = "Нужен секретный ключ (sk_live_…, не pk_live_…)" }
|
||||||
else if code == 0 { errMsg = error?.localizedDescription ?? "No connection" }
|
else if code == 0 { errMsg = error?.localizedDescription ?? "Нет соединения" }
|
||||||
else { errMsg = "API error \(code)" }
|
else { errMsg = "Ошибка API \(code)" }
|
||||||
DispatchQueue.main.async { AppState.shared.stripeError = errMsg }
|
DispatchQueue.main.async { AppState.shared.stripeError = errMsg }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -160,12 +160,12 @@ struct UploadCanvasView: View {
|
|||||||
var tCtx = ctx
|
var tCtx = ctx
|
||||||
tCtx.opacity = f.textAlpha
|
tCtx.opacity = f.textAlpha
|
||||||
|
|
||||||
let label = Text("Drop your files here")
|
let label = Text("Перетащи файлы сюда")
|
||||||
.font(.system(size:13, weight:.medium))
|
.font(.system(size:13, weight:.medium))
|
||||||
.foregroundColor(Color(hex:"#D5D7DB"))
|
.foregroundColor(Color(hex:"#D5D7DB"))
|
||||||
tCtx.draw(label, at: CGPoint(x: USC.TEXT_X, y: USC.TEXT_Y - 4), anchor: .leading)
|
tCtx.draw(label, at: CGPoint(x: USC.TEXT_X, y: USC.TEXT_Y - 4), anchor: .leading)
|
||||||
|
|
||||||
let chips = ["PDF","Images","Code","Docs"]
|
let chips = ["PDF","Фото","Код","Документы"]
|
||||||
var cx = USC.TEXT_X
|
var cx = USC.TEXT_X
|
||||||
for chip in chips {
|
for chip in chips {
|
||||||
let chipText = Text(chip).font(.system(size:11, weight:.medium)).foregroundColor(Color(hex:"#B9BDC4"))
|
let chipText = Text(chip).font(.system(size:11, weight:.medium)).foregroundColor(Color(hex:"#B9BDC4"))
|
||||||
@@ -188,8 +188,8 @@ struct UploadCanvasView: View {
|
|||||||
let barLen = (x1-x0) * f.barReveal
|
let barLen = (x1-x0) * f.barReveal
|
||||||
|
|
||||||
// Filename label
|
// Filename label
|
||||||
let name = state.droppedFile?.name ?? "file"
|
let name = state.droppedFile?.name ?? "файл"
|
||||||
let label = Text("Uploading \(name)")
|
let label = Text("Загружаю \(name)")
|
||||||
.font(.system(size:12.5, weight:.medium))
|
.font(.system(size:12.5, weight:.medium))
|
||||||
.foregroundColor(Color(hex:"#A9ADB5"))
|
.foregroundColor(Color(hex:"#A9ADB5"))
|
||||||
pCtx.draw(label, at: CGPoint(x: x0, y: by-30), anchor: .leading)
|
pCtx.draw(label, at: CGPoint(x: x0, y: by-30), anchor: .leading)
|
||||||
@@ -262,13 +262,13 @@ struct UploadCanvasView: View {
|
|||||||
// Slide up: translate down by (1-alpha)*4
|
// Slide up: translate down by (1-alpha)*4
|
||||||
cCtx.concatenate(CGAffineTransform(translationX: 0, y: CGFloat((1-f.chooseAlpha)*4)))
|
cCtx.concatenate(CGAffineTransform(translationX: 0, y: CGFloat((1-f.chooseAlpha)*4)))
|
||||||
|
|
||||||
let name = state.droppedFile?.name ?? "file"
|
let name = state.droppedFile?.name ?? "Файл"
|
||||||
let titleText = Text("\(name) is ready.")
|
let titleText = Text("\(name) готов.")
|
||||||
.font(.system(size:14, weight:.semibold))
|
.font(.system(size:14, weight:.semibold))
|
||||||
.foregroundColor(Color(hex:"#F5F6F8"))
|
.foregroundColor(Color(hex:"#F5F6F8"))
|
||||||
cCtx.draw(titleText, at: CGPoint(x:114, y:80), anchor: .leading)
|
cCtx.draw(titleText, at: CGPoint(x:114, y:80), anchor: .leading)
|
||||||
|
|
||||||
let subText = Text("What do you want to do with it?")
|
let subText = Text("Что с ним сделать?")
|
||||||
.font(.system(size:12.5))
|
.font(.system(size:12.5))
|
||||||
.foregroundColor(Color(hex:"#9398A1"))
|
.foregroundColor(Color(hex:"#9398A1"))
|
||||||
cCtx.draw(subText, at: CGPoint(x:114, y:100), anchor: .leading)
|
cCtx.draw(subText, at: CGPoint(x:114, y:100), anchor: .leading)
|
||||||
@@ -276,7 +276,7 @@ struct UploadCanvasView: View {
|
|||||||
// Primary button (white fill)
|
// Primary button (white fill)
|
||||||
cCtx.fill(roundedRect(CGRect(x:114,y:113,width:168,height:26), r:13),
|
cCtx.fill(roundedRect(CGRect(x:114,y:113,width:168,height:26), r:13),
|
||||||
with: .color(Color(hex:"#F5F6F8")))
|
with: .color(Color(hex:"#F5F6F8")))
|
||||||
let btn1 = Text("Ask a question about it")
|
let btn1 = Text("Задать вопрос")
|
||||||
.font(.system(size:12.5, weight:.medium))
|
.font(.system(size:12.5, weight:.medium))
|
||||||
.foregroundColor(Color(red:0.043,green:0.047,blue:0.055))
|
.foregroundColor(Color(red:0.043,green:0.047,blue:0.055))
|
||||||
cCtx.draw(btn1, at: CGPoint(x:198, y:126), anchor: .center)
|
cCtx.draw(btn1, at: CGPoint(x:198, y:126), anchor: .center)
|
||||||
@@ -284,7 +284,7 @@ struct UploadCanvasView: View {
|
|||||||
// Secondary button (dim fill)
|
// Secondary button (dim fill)
|
||||||
cCtx.fill(roundedRect(CGRect(x:290,y:113,width:120,height:26), r:13),
|
cCtx.fill(roundedRect(CGRect(x:290,y:113,width:120,height:26), r:13),
|
||||||
with: .color(Color.white.opacity(0.09)))
|
with: .color(Color.white.opacity(0.09)))
|
||||||
let btn2 = Text("Send by email")
|
let btn2 = Text("Отправить почтой")
|
||||||
.font(.system(size:12.5, weight:.medium))
|
.font(.system(size:12.5, weight:.medium))
|
||||||
.foregroundColor(Color(hex:"#F1F2F4"))
|
.foregroundColor(Color(hex:"#F1F2F4"))
|
||||||
cCtx.draw(btn2, at: CGPoint(x:350, y:126), anchor: .center)
|
cCtx.draw(btn2, at: CGPoint(x:350, y:126), anchor: .center)
|
||||||
|
|||||||
@@ -46,24 +46,24 @@ struct MochiActivityState: Codable, Hashable, Sendable {
|
|||||||
|
|
||||||
static func statusText(state: BotState, urgency: Int, stepIndex: Int, stepCount: Int) -> String {
|
static func statusText(state: BotState, urgency: Int, stepIndex: Int, stepCount: Int) -> String {
|
||||||
switch urgency {
|
switch urgency {
|
||||||
case 0: return "waiting for your OK"
|
case 0: return "ждёт вашего OK"
|
||||||
case 1: return "has a question"
|
case 1: return "есть вопрос"
|
||||||
default: break
|
default: break
|
||||||
}
|
}
|
||||||
switch state {
|
switch state {
|
||||||
case .working, .thinking, .searching:
|
case .working, .thinking, .searching:
|
||||||
return stepCount == 0 ? "working" : "working · \(min(stepIndex + 1, stepCount))/\(stepCount)"
|
return stepCount == 0 ? "работает" : "работает · \(min(stepIndex + 1, stepCount))/\(stepCount)"
|
||||||
case .finished: return "✓ done"
|
case .finished: return "✓ готово"
|
||||||
case .error: return "error"
|
case .error: return "ошибка"
|
||||||
case .ratelimit: return "rate limited"
|
case .ratelimit: return "лимит запросов"
|
||||||
case .sleeping: return "asleep"
|
case .sleeping: return "спит"
|
||||||
case .dizzy: return "dizzy"
|
case .dizzy: return "голова кружится"
|
||||||
default: return "idle"
|
default: return "отдыхает"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static let placeholder = MochiActivityState(
|
static let placeholder = MochiActivityState(
|
||||||
pillId: "integration_claude", agent: "VS Code", color: "#4A86E8",
|
pillId: "integration_claude", agent: "VS Code", color: "#4A86E8",
|
||||||
state: "working", statusText: "working · 3/7", tone: "working",
|
state: "working", statusText: "работает · 3/7", tone: "working",
|
||||||
stepIndex: 2, stepCount: 7, others: 1)
|
stepIndex: 2, stepCount: 7, others: 1)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,19 +6,19 @@ enum Outfit: String, CaseIterable {
|
|||||||
|
|
||||||
var displayName: String {
|
var displayName: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .auto: return "Auto (seasons)"
|
case .auto: return "Авто (по сезону)"
|
||||||
case .none: return "None"
|
case .none: return "Без наряда"
|
||||||
case .partyHat: return "Party hat"
|
case .partyHat: return "Праздничный колпак"
|
||||||
case .beanie: return "Beanie"
|
case .beanie: return "Шапка"
|
||||||
case .crown: return "Crown"
|
case .crown: return "Корона"
|
||||||
case .sunglasses: return "Sunglasses"
|
case .sunglasses: return "Солнечные очки"
|
||||||
case .roundGlasses: return "Round glasses"
|
case .roundGlasses: return "Круглые очки"
|
||||||
case .bow: return "Bow"
|
case .bow: return "Бантик"
|
||||||
case .scarf: return "Scarf"
|
case .scarf: return "Шарф"
|
||||||
case .witchHat: return "Witch hat"
|
case .witchHat: return "Шляпа ведьмы"
|
||||||
case .pumpkin: return "Pumpkin"
|
case .pumpkin: return "Тыква"
|
||||||
case .santaHat: return "Santa hat"
|
case .santaHat: return "Шапка Санты"
|
||||||
case .bunnyEars: return "Bunny ears"
|
case .bunnyEars: return "Заячьи ушки"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ enum PillCategory: String, CaseIterable {
|
|||||||
|
|
||||||
var title: String {
|
var title: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .workspace: return "Where you code"
|
case .workspace: return "Где вы пишете код"
|
||||||
case .agent: return "Agents"
|
case .agent: return "Агенты"
|
||||||
case .ai: return "AI for the chat"
|
case .ai: return "ИИ для чата"
|
||||||
case .service: return "Services"
|
case .service: return "Сервисы"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -37,7 +37,7 @@ struct PillDefinition {
|
|||||||
case "integration_claude": return "Claude Code"
|
case "integration_claude": return "Claude Code"
|
||||||
case "agent_cursor": return "Cursor"
|
case "agent_cursor": return "Cursor"
|
||||||
case "agent_codex": return "Codex"
|
case "agent_codex": return "Codex"
|
||||||
default: return "Agent"
|
default: return "Агент"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -49,44 +49,44 @@ enum PillCatalog {
|
|||||||
static let all: [PillDefinition] = [
|
static let all: [PillDefinition] = [
|
||||||
// ── Where you code ───────────────────────────────────────────────────
|
// ── Where you code ───────────────────────────────────────────────────
|
||||||
.init(id: "integration_claude", name: "VS Code", color: "#F5F6F8",
|
.init(id: "integration_claude", name: "VS Code", color: "#F5F6F8",
|
||||||
category: .workspace, subtitle: "Integration", source: .claudeCode),
|
category: .workspace, subtitle: "Интеграция", source: .claudeCode),
|
||||||
.init(id: "agent_cursor", name: "Cursor", color: "#C0C4CC",
|
.init(id: "agent_cursor", name: "Cursor", color: "#C0C4CC",
|
||||||
category: .workspace, subtitle: "Integration", source: .agent),
|
category: .workspace, subtitle: "Интеграция", source: .agent),
|
||||||
.init(id: "agent_antigravity", name: "Antigravity", color: "#E879F9",
|
.init(id: "agent_antigravity", name: "Antigravity", color: "#E879F9",
|
||||||
category: .workspace, subtitle: "Integration", source: .agent, githubOnly: true),
|
category: .workspace, subtitle: "Интеграция", source: .agent, githubOnly: true),
|
||||||
.init(id: "agent_codex", name: "Codex", color: "#2DD4BF",
|
.init(id: "agent_codex", name: "Codex", color: "#2DD4BF",
|
||||||
category: .workspace, subtitle: "Integration", source: .agent, githubOnly: true),
|
category: .workspace, subtitle: "Интеграция", source: .agent, githubOnly: true),
|
||||||
// ── Agents ───────────────────────────────────────────────────────────
|
// ── Agents ───────────────────────────────────────────────────────────
|
||||||
.init(id: "agent_gemini", name: "Gemini CLI", color: "#8AB4F8",
|
.init(id: "agent_gemini", name: "Gemini CLI", color: "#8AB4F8",
|
||||||
category: .agent, subtitle: "Agent", source: .agent, githubOnly: true),
|
category: .agent, subtitle: "Агент", source: .agent, githubOnly: true),
|
||||||
// ── AI for the chat ──────────────────────────────────────────────────
|
// ── AI for the chat ──────────────────────────────────────────────────
|
||||||
.init(id: "ai_anthropic", name: "Anthropic", color: ChatProvider.anthropic.accentHex,
|
.init(id: "ai_anthropic", name: "Anthropic", color: ChatProvider.anthropic.accentHex,
|
||||||
category: .ai, subtitle: "Chat", source: .n8n),
|
category: .ai, subtitle: "Чат", source: .n8n),
|
||||||
.init(id: "ai_google", name: "Google AI", color: ChatProvider.google.accentHex,
|
.init(id: "ai_google", name: "Google AI", color: ChatProvider.google.accentHex,
|
||||||
category: .ai, subtitle: "Chat", source: .n8n),
|
category: .ai, subtitle: "Чат", source: .n8n),
|
||||||
.init(id: "ai_openai", name: "OpenAI", color: ChatProvider.openai.accentHex,
|
.init(id: "ai_openai", name: "OpenAI", color: ChatProvider.openai.accentHex,
|
||||||
category: .ai, subtitle: "Chat", source: .n8n),
|
category: .ai, subtitle: "Чат", source: .n8n),
|
||||||
.init(id: "ai_ollama", name: "Ollama", color: ChatProvider.ollama.accentHex,
|
.init(id: "ai_ollama", name: "Ollama", color: ChatProvider.ollama.accentHex,
|
||||||
category: .ai, subtitle: "Chat", source: .n8n),
|
category: .ai, subtitle: "Чат", source: .n8n),
|
||||||
.init(id: "ai_lmstudio", name: "LM Studio", color: ChatProvider.lmstudio.accentHex,
|
.init(id: "ai_lmstudio", name: "LM Studio", color: ChatProvider.lmstudio.accentHex,
|
||||||
category: .ai, subtitle: "Chat", source: .n8n),
|
category: .ai, subtitle: "Чат", source: .n8n),
|
||||||
// ── Services ─────────────────────────────────────────────────────────
|
// ── Services ─────────────────────────────────────────────────────────
|
||||||
.init(id: "integration_resend", name: "Resend", color: "#22C55E",
|
.init(id: "integration_resend", name: "Resend", color: "#22C55E",
|
||||||
category: .service, subtitle: "Integration", source: .n8n),
|
category: .service, subtitle: "Интеграция", source: .n8n),
|
||||||
.init(id: "integration_n8n", name: "n8n", color: "#F29B38",
|
.init(id: "integration_n8n", name: "n8n", color: "#F29B38",
|
||||||
category: .service, subtitle: "Integration", source: .n8n),
|
category: .service, subtitle: "Интеграция", source: .n8n),
|
||||||
.init(id: "integration_vercel", name: "Vercel", color: "#7C5CFF",
|
.init(id: "integration_vercel", name: "Vercel", color: "#7C5CFF",
|
||||||
category: .service, subtitle: "Integration", source: .n8n),
|
category: .service, subtitle: "Интеграция", source: .n8n),
|
||||||
.init(id: "integration_github", name: "GitHub", color: "#F4505E",
|
.init(id: "integration_github", name: "GitHub", color: "#F4505E",
|
||||||
category: .service, subtitle: "Integration", source: .n8n),
|
category: .service, subtitle: "Интеграция", source: .n8n),
|
||||||
.init(id: "integration_notion", name: "Notion", color: "#8C8C8C",
|
.init(id: "integration_notion", name: "Notion", color: "#8C8C8C",
|
||||||
category: .service, subtitle: "Integration", source: .n8n),
|
category: .service, subtitle: "Интеграция", source: .n8n),
|
||||||
.init(id: "integration_calcom", name: "Cal.com", color: "#C9956A",
|
.init(id: "integration_calcom", name: "Cal.com", color: "#C9956A",
|
||||||
category: .service, subtitle: "Integration", source: .n8n),
|
category: .service, subtitle: "Интеграция", source: .n8n),
|
||||||
.init(id: "integration_stripe", name: "Stripe", color: "#0570DE",
|
.init(id: "integration_stripe", name: "Stripe", color: "#0570DE",
|
||||||
category: .service, subtitle: "Integration", source: .n8n),
|
category: .service, subtitle: "Интеграция", source: .n8n),
|
||||||
.init(id: "integration_music", name: "Apple Music", color: "#FA2D48",
|
.init(id: "integration_music", name: "Apple Music", color: "#FA2D48",
|
||||||
category: .service, subtitle: "Integration", source: .n8n, githubOnly: true),
|
category: .service, subtitle: "Интеграция", source: .n8n, githubOnly: true),
|
||||||
]
|
]
|
||||||
|
|
||||||
/// Pills available in the current build target.
|
/// Pills available in the current build target.
|
||||||
|
|||||||
Reference in New Issue
Block a user