From 1ca6b6ff6722431d35b9fc2d25c6a5c91beb67dd Mon Sep 17 00:00:00 2001 From: Colin Johnson Date: Sat, 18 Jul 2026 14:30:10 -0400 Subject: [PATCH] feat(ios): unify chat and voice experience (#107879) * feat(ios): unify chat and voice experience * refactor(ios): finish unified chat ownership * fix(ios): unify chat and voice capture ownership * fix(ios): serialize Talk permission hydration * fix(ios): clear voice CI regressions * fix(ios): clear remaining unified chat CI failures * chore(i18n): sync native source inventory * refactor(ios): defer provider-only realtime voice * fix(ios): clean unified chat voice PR * fix(ios): localize unified voice controls * fix(ios): serialize composer audio controls * fix(ios): isolate dictation preparation lifecycle * fix(ios): key chat tab icons by appearance * fix(ios): preserve talk upgrade handshakes * fix(ios): refresh native i18n inventory * fix(ios): reconcile shared locale artifacts * fix(ios): satisfy chat view lint limit * fix(ios): reconcile shared composer implementation * test(apple): avoid XCTest actor deinit crash * fix(ios): satisfy SwiftFormat scope spacing * fix(ios): remove duplicate merged helper declarations --- .../apple-translation-contradictions.json | 379 +- apps/.i18n/native-source.json | 1288 ++-- apps/.i18n/native/ar.json | 440 +- apps/.i18n/native/de.json | 440 +- apps/.i18n/native/es.json | 440 +- apps/.i18n/native/fa.json | 440 +- apps/.i18n/native/fr.json | 440 +- apps/.i18n/native/hi.json | 440 +- apps/.i18n/native/id.json | 440 +- apps/.i18n/native/it.json | 440 +- apps/.i18n/native/ja-JP.json | 440 +- apps/.i18n/native/ko.json | 440 +- apps/.i18n/native/nl.json | 440 +- apps/.i18n/native/pl.json | 440 +- apps/.i18n/native/pt-BR.json | 440 +- apps/.i18n/native/ru.json | 440 +- apps/.i18n/native/sv.json | 440 +- apps/.i18n/native/th.json | 440 +- apps/.i18n/native/tr.json | 440 +- apps/.i18n/native/uk.json | 440 +- apps/.i18n/native/vi.json | 440 +- apps/.i18n/native/zh-CN.json | 440 +- apps/.i18n/native/zh-TW.json | 440 +- .../app/src/main/res/values-ar/strings.xml | 2 +- .../app/src/main/res/values-it/strings.xml | 2 +- .../app/src/main/res/values-ja/strings.xml | 2 +- .../app/src/main/res/values-pl/strings.xml | 2 +- .../app/src/main/res/values-tr/strings.xml | 2 +- .../src/main/res/values-zh-rTW/strings.xml | 2 +- .../OpenClawActivityTypography.swift | 4 + .../ActivityWidget/OpenClawLiveActivity.swift | 222 +- apps/ios/Resources/Localizable.xcstrings | 5582 +++++++---------- apps/ios/Sources/Design/ChatProTab.swift | 344 +- apps/ios/Sources/Design/OpenClawBrand.swift | 29 + .../Design/OpenClawProComponents.swift | 10 - .../Sources/Design/RootTabsPhoneChrome.swift | 114 + .../Design/RootTabsPhoneControlHub.swift | 25 +- .../Design/SettingsProTabActions.swift | 1 + apps/ios/Sources/Design/TalkProTab.swift | 484 -- .../LiveActivity/LiveActivityManager.swift | 601 +- .../LiveActivityPresentationArbiter.swift | 150 + .../OpenClawActivityAttributes.swift | 36 +- .../VoiceLiveActivityCoordinator.swift | 85 + .../Model/AgentIdentityPresentation.swift | 21 + apps/ios/Sources/Model/NodeAppModel.swift | 325 +- .../Onboarding/OnboardingWizardView.swift | 2 + apps/ios/Sources/OpenClawApp.swift | 16 + apps/ios/Sources/RootTabs.swift | 189 +- apps/ios/Sources/RootTabsCanvasPayload.swift | 21 + apps/ios/Sources/RootTabsNavigation.swift | 17 +- .../Voice/TalkGatewayPermissionState.swift | 16 +- apps/ios/Sources/Voice/TalkModeManager.swift | 295 +- .../Voice/TalkPermissionPromptView.swift | 178 - .../GatewayConnectionControllerTests.swift | 12 + .../ios/Tests/GatewayStatusBuilderTests.swift | 43 +- ...LiveActivityPresentationArbiterTests.swift | 229 + apps/ios/Tests/NodeAppModelInvokeTests.swift | 295 + .../ios/Tests/RootTabsPresentationTests.swift | 35 +- ...tTabsSourceGuardTests+GatewaySupport.swift | 4 +- apps/ios/Tests/RootTabsSourceGuardTests.swift | 114 +- .../RuntimeLocalizationSourceGuardTests.swift | 34 +- .../Tests/TalkModeConfigParsingTests.swift | 5 +- apps/ios/Tests/TalkProStateTests.swift | 145 - .../ios/UITests/OpenClawSnapshotUITests.swift | 113 +- apps/ios/project.yml | 3 + .../Sources/OpenClawChatUI/ChatComposer.swift | 743 ++- .../Sources/OpenClawChatUI/ChatView.swift | 14 +- .../ChatViewModel+Attachments.swift | 131 +- .../ChatViewModel+Sending.swift | 325 +- .../ChatViewModel+ToolActivity.swift | 25 + .../ChatViewModel+TranscriptCache.swift | 2 +- .../OpenClawChatUI/ChatViewModel.swift | 102 +- .../CleanChatComposerControls.swift | 337 + .../OpenClawChatDictationControl.swift | 38 + .../OpenClawChatUI/TalkWaveformView.swift | 426 +- .../VoiceNoteComposerViews.swift | 20 +- .../ChatComposerParityTests.swift | 18 + .../OpenClawKitTests/ChatViewModelTests.swift | 264 +- .../OpenClawKitTests/TalkWaveformTests.swift | 85 + .../VoiceNoteRecorderTests.swift | 95 + test/scripts/native-app-i18n.test.ts | 8 - 81 files changed, 11763 insertions(+), 11588 deletions(-) create mode 100644 apps/ios/Sources/Design/RootTabsPhoneChrome.swift delete mode 100644 apps/ios/Sources/Design/TalkProTab.swift create mode 100644 apps/ios/Sources/LiveActivity/LiveActivityPresentationArbiter.swift create mode 100644 apps/ios/Sources/LiveActivity/VoiceLiveActivityCoordinator.swift create mode 100644 apps/ios/Sources/Model/AgentIdentityPresentation.swift create mode 100644 apps/ios/Sources/RootTabsCanvasPayload.swift delete mode 100644 apps/ios/Sources/Voice/TalkPermissionPromptView.swift create mode 100644 apps/ios/Tests/LiveActivityPresentationArbiterTests.swift delete mode 100644 apps/ios/Tests/TalkProStateTests.swift create mode 100644 apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel+ToolActivity.swift create mode 100644 apps/shared/OpenClawKit/Sources/OpenClawChatUI/CleanChatComposerControls.swift create mode 100644 apps/shared/OpenClawKit/Sources/OpenClawChatUI/OpenClawChatDictationControl.swift diff --git a/apps/.i18n/apple-translation-contradictions.json b/apps/.i18n/apple-translation-contradictions.json index c2ed401a1164..2945368ae999 100644 --- a/apps/.i18n/apple-translation-contradictions.json +++ b/apps/.i18n/apple-translation-contradictions.json @@ -270,7 +270,6 @@ "locale": "uk", "source": "Active", "translations": [ - "Активний", "Активно", "Активні" ] @@ -279,7 +278,6 @@ "locale": "zh-CN", "source": "Active", "translations": [ - "活动", "活跃", "进行中" ] @@ -314,8 +312,7 @@ "source": "Agent", "translations": [ "Agent", - "代理", - "智能体" + "代理" ] }, { @@ -1058,6 +1055,14 @@ "Kamera" ] }, + { + "locale": "vi", + "source": "Camera", + "translations": [ + "Camera", + "Máy ảnh" + ] + }, { "locale": "zh-CN", "source": "Camera", @@ -1212,30 +1217,6 @@ "指令" ] }, - { - "locale": "ar", - "source": "Configured", - "translations": [ - "مهيأ", - "مُهيأ" - ] - }, - { - "locale": "fa", - "source": "Configured", - "translations": [ - "پیکربندی شده", - "پیکربندی‌شده" - ] - }, - { - "locale": "ja-JP", - "source": "Configured", - "translations": [ - "構成済み", - "設定済み" - ] - }, { "locale": "fr", "source": "Connect", @@ -2774,78 +2755,6 @@ "有効化" ] }, - { - "locale": "ar", - "source": "Enable Talk", - "translations": [ - "تفعيل Talk", - "تمكين التحدث" - ] - }, - { - "locale": "ja-JP", - "source": "Enable Talk", - "translations": [ - "Talk を有効にする", - "トークを有効化" - ] - }, - { - "locale": "nl", - "source": "Enable Talk", - "translations": [ - "Gesprek inschakelen", - "Talk inschakelen" - ] - }, - { - "locale": "pt-BR", - "source": "Enable Talk", - "translations": [ - "Ativar Talk", - "Ativar conversa" - ] - }, - { - "locale": "ru", - "source": "Enable Talk", - "translations": [ - "Включить Talk", - "Включить разговор" - ] - }, - { - "locale": "tr", - "source": "Enable Talk", - "translations": [ - "Konuşmayı Etkinleştir", - "Talk’u etkinleştir" - ] - }, - { - "locale": "uk", - "source": "Enable Talk", - "translations": [ - "Увімкнути Talk", - "Увімкнути розмову" - ] - }, - { - "locale": "zh-CN", - "source": "Enable Talk", - "translations": [ - "启用 Talk", - "启用对话" - ] - }, - { - "locale": "zh-TW", - "source": "Enable Talk", - "translations": [ - "啟用 Talk", - "啟用對話" - ] - }, { "locale": "es", "source": "Enabled", @@ -4099,6 +4008,22 @@ "列出、新增及完成提醒事項。" ] }, + { + "locale": "fa", + "source": "Listening", + "translations": [ + "در حال گوش دادن", + "در حال گوش‌دادن" + ] + }, + { + "locale": "hi", + "source": "Listening", + "translations": [ + "सुन रहा है", + "सुना जा रहा है" + ] + }, { "locale": "ja-JP", "source": "Listening", @@ -4115,12 +4040,21 @@ "Luistert" ] }, + { + "locale": "pl", + "source": "Listening", + "translations": [ + "Nasłuchiwanie", + "Słuchanie" + ] + }, { "locale": "uk", "source": "Listening", "translations": [ "Прослуховування", - "Слухання" + "Слухання", + "Слухаю" ] }, { @@ -5935,22 +5869,6 @@ "線上" ] }, - { - "locale": "fr", - "source": "Open Gateway Settings", - "translations": [ - "Ouvrir les paramètres de Gateway", - "Ouvrir les paramètres du Gateway" - ] - }, - { - "locale": "vi", - "source": "Open Gateway Settings", - "translations": [ - "Mở Cài đặt Gateway", - "Mở cài đặt Gateway" - ] - }, { "locale": "es", "source": "Open Settings", @@ -6381,6 +6299,14 @@ "متوقفة مؤقتًا" ] }, + { + "locale": "hi", + "source": "Paused", + "translations": [ + "रोका गया", + "रोके गए" + ] + }, { "locale": "ko", "source": "Paused", @@ -6413,6 +6339,14 @@ "Приостановлено" ] }, + { + "locale": "sv", + "source": "Paused", + "translations": [ + "Pausad", + "Pausade" + ] + }, { "locale": "uk", "source": "Paused", @@ -7359,7 +7293,6 @@ "source": "Retry", "translations": [ "Retry", - "تلاش دوباره", "تلاش مجدد" ] }, @@ -7425,7 +7358,6 @@ "locale": "pl", "source": "Retry", "translations": [ - "Ponów", "Ponów próbę", "Retry", "Spróbuj ponownie" @@ -7693,54 +7625,6 @@ "运行中" ] }, - { - "locale": "ar", - "source": "Runtime", - "translations": [ - "بيئة التشغيل", - "وقت التشغيل" - ] - }, - { - "locale": "es", - "source": "Runtime", - "translations": [ - "Entorno de ejecución", - "Runtime" - ] - }, - { - "locale": "fa", - "source": "Runtime", - "translations": [ - "زمان اجرا", - "محیط اجرا" - ] - }, - { - "locale": "pt-BR", - "source": "Runtime", - "translations": [ - "Ambiente de execução", - "Tempo de execução" - ] - }, - { - "locale": "tr", - "source": "Runtime", - "translations": [ - "Çalışma Zamanı", - "Çalışma zamanı" - ] - }, - { - "locale": "vi", - "source": "Runtime", - "translations": [ - "Môi trường chạy", - "Thời gian chạy" - ] - }, { "locale": "de", "source": "Same Machine (Dev)", @@ -8441,46 +8325,6 @@ "Skill 工作坊" ] }, - { - "locale": "de", - "source": "Speakerphone", - "translations": [ - "Freisprechen", - "Freisprecher" - ] - }, - { - "locale": "hi", - "source": "Speakerphone", - "translations": [ - "Speakerphone", - "स्पीकरफ़ोन" - ] - }, - { - "locale": "th", - "source": "Speakerphone", - "translations": [ - "ลำโพงโทรศัพท์", - "สปีกเกอร์โฟน" - ] - }, - { - "locale": "uk", - "source": "Speakerphone", - "translations": [ - "Гучний зв’язок", - "Гучномовець" - ] - }, - { - "locale": "zh-TW", - "source": "Speakerphone", - "translations": [ - "免持聽筒", - "擴音" - ] - }, { "locale": "ar", "source": "Speaking", @@ -8633,6 +8477,14 @@ "Озвучення…" ] }, + { + "locale": "zh-TW", + "source": "Speaking…", + "translations": [ + "正在朗讀…", + "說話中…" + ] + }, { "locale": "hi", "source": "Speech recognition", @@ -8793,99 +8645,6 @@ "Статус" ] }, - { - "locale": "ar", - "source": "Talk", - "translations": [ - "التحدث", - "تحدث", - "تحدّث" - ] - }, - { - "locale": "fa", - "source": "Talk", - "translations": [ - "صحبت", - "گفتگو", - "گفت‌وگو" - ] - }, - { - "locale": "ja-JP", - "source": "Talk", - "translations": [ - "トーク", - "会話", - "話す" - ] - }, - { - "locale": "ko", - "source": "Talk", - "translations": [ - "대화", - "대화하기" - ] - }, - { - "locale": "pl", - "source": "Talk", - "translations": [ - "Rozmawiaj", - "Rozmowa" - ] - }, - { - "locale": "pt-BR", - "source": "Talk", - "translations": [ - "Conversar", - "Falar" - ] - }, - { - "locale": "ru", - "source": "Talk", - "translations": [ - "Говорить", - "Разговор" - ] - }, - { - "locale": "uk", - "source": "Talk", - "translations": [ - "Говорити", - "Розмова" - ] - }, - { - "locale": "vi", - "source": "Talk", - "translations": [ - "Nói chuyện", - "Trò chuyện" - ] - }, - { - "locale": "zh-CN", - "source": "Talk", - "translations": [ - "对话", - "通话" - ] - }, - { - "locale": "zh-TW", - "source": "Talk", - "translations": [ - "交談", - "對話", - "語音對話", - "說話" - ] - }, { "locale": "ar", "source": "Technical details", @@ -9307,14 +9066,6 @@ "Назва" ] }, - { - "locale": "vi", - "source": "Transport", - "translations": [ - "Phương thức truyền tải", - "Truyền tải" - ] - }, { "locale": "it", "source": "Unarchive", @@ -9620,14 +9371,6 @@ "Röst och tal" ] }, - { - "locale": "uk", - "source": "Voice Mode", - "translations": [ - "Голосовий режим", - "Режим голосу" - ] - }, { "locale": "de", "source": "Voice Wake", @@ -9761,7 +9504,7 @@ "source": "Voice note", "translations": [ "Voice note", - "ボイスメモ" + "ボイスノート" ] }, { @@ -9817,7 +9560,7 @@ "source": "Voice note", "translations": [ "Voice note", - "บันทึกเสียง" + "ข้อความเสียง" ] }, { diff --git a/apps/.i18n/native-source.json b/apps/.i18n/native-source.json index 7b7b289af154..e2eff5f44a90 100644 --- a/apps/.i18n/native-source.json +++ b/apps/.i18n/native-source.json @@ -13371,7 +13371,15 @@ }, { "kind": "ui-call", - "line": 39, + "line": 18, + "path": "apps/ios/ActivityWidget/OpenClawLiveActivity.swift", + "source": "OPENCLAW", + "surface": "apple", + "id": "native.apple.093081590556db7f" + }, + { + "kind": "ui-call", + "line": 55, "path": "apps/ios/ActivityWidget/OpenClawLiveActivity.swift", "source": "OpenClaw", "surface": "apple", @@ -13379,7 +13387,15 @@ }, { "kind": "ui-call", - "line": 96, + "line": 132, + "path": "apps/ios/ActivityWidget/OpenClawLiveActivity.swift", + "source": "LIVE", + "surface": "apple", + "id": "native.apple.9fd01bdb5a753f3e" + }, + { + "kind": "ui-call", + "line": 210, "path": "apps/ios/ActivityWidget/OpenClawLiveActivity.swift", "source": "Connecting...", "surface": "apple", @@ -13387,7 +13403,7 @@ }, { "kind": "ui-call", - "line": 97, + "line": 211, "path": "apps/ios/ActivityWidget/OpenClawLiveActivity.swift", "source": "Reconnecting...", "surface": "apple", @@ -13395,7 +13411,7 @@ }, { "kind": "ui-call", - "line": 98, + "line": 212, "path": "apps/ios/ActivityWidget/OpenClawLiveActivity.swift", "source": "Approval needed", "surface": "apple", @@ -13403,15 +13419,63 @@ }, { "kind": "ui-call", - "line": 99, + "line": 213, "path": "apps/ios/ActivityWidget/OpenClawLiveActivity.swift", "source": "Action required", "surface": "apple", "id": "native.apple.c619843217c15317" }, + { + "kind": "ui-localized-call", + "line": 216, + "path": "apps/ios/ActivityWidget/OpenClawLiveActivity.swift", + "source": "Using %@", + "surface": "apple", + "id": "native.apple.275026f681242715" + }, { "kind": "ui-call", - "line": 100, + "line": 218, + "path": "apps/ios/ActivityWidget/OpenClawLiveActivity.swift", + "source": "Using a tool", + "surface": "apple", + "id": "native.apple.360d0d0fc0df9eb2" + }, + { + "kind": "ui-call", + "line": 220, + "path": "apps/ios/ActivityWidget/OpenClawLiveActivity.swift", + "source": "Listening", + "surface": "apple", + "id": "native.apple.0d0ee660962ea840" + }, + { + "kind": "ui-call", + "line": 221, + "path": "apps/ios/ActivityWidget/OpenClawLiveActivity.swift", + "source": "Speaking", + "surface": "apple", + "id": "native.apple.095ebf1496feb2ad" + }, + { + "kind": "ui-call", + "line": 222, + "path": "apps/ios/ActivityWidget/OpenClawLiveActivity.swift", + "source": "Voice active", + "surface": "apple", + "id": "native.apple.63c98d2ca6a5b09c" + }, + { + "kind": "ui-call", + "line": 223, + "path": "apps/ios/ActivityWidget/OpenClawLiveActivity.swift", + "source": "Paused", + "surface": "apple", + "id": "native.apple.02470f4b5348f97c" + }, + { + "kind": "ui-call", + "line": 224, "path": "apps/ios/ActivityWidget/OpenClawLiveActivity.swift", "source": "Connected", "surface": "apple", @@ -13419,7 +13483,7 @@ }, { "kind": "ui-call", - "line": 101, + "line": 225, "path": "apps/ios/ActivityWidget/OpenClawLiveActivity.swift", "source": "Disconnected", "surface": "apple", @@ -16787,7 +16851,7 @@ }, { "kind": "ui-localized-call", - "line": 159, + "line": 173, "path": "apps/ios/Sources/Design/ChatProTab.swift", "source": "Unable to Export Transcript", "surface": "apple", @@ -16795,7 +16859,7 @@ }, { "kind": "ui-call", - "line": 163, + "line": 177, "path": "apps/ios/Sources/Design/ChatProTab.swift", "source": "OK", "surface": "apple", @@ -16803,7 +16867,7 @@ }, { "kind": "ui-call", - "line": 167, + "line": 181, "path": "apps/ios/Sources/Design/ChatProTab.swift", "source": "OpenClaw could not prepare the Markdown file.", "surface": "apple", @@ -16811,7 +16875,7 @@ }, { "kind": "ui-localized-call", - "line": 189, + "line": 203, "path": "apps/ios/Sources/Design/ChatProTab.swift", "source": "What would you like to work on?", "surface": "apple", @@ -16819,23 +16883,63 @@ }, { "kind": "ui-call", - "line": 202, + "line": 217, "path": "apps/ios/Sources/Design/ChatProTab.swift", "source": "The session attaches once the gateway is ready.", "surface": "apple", "id": "native.apple.32179ce0b6d4543f" }, { - "kind": "ui-modifier", - "line": 330, + "kind": "conditional-branch", + "line": 305, + "path": "apps/ios/Sources/Design/ChatProTab.swift", + "source": "Collapsed", + "surface": "apple", + "id": "native.apple.02611ddddef7295a" + }, + { + "kind": "conditional-branch", + "line": 305, + "path": "apps/ios/Sources/Design/ChatProTab.swift", + "source": "Expanded", + "surface": "apple", + "id": "native.apple.1f4911f4a05230b5" + }, + { + "kind": "ui-localized-call", + "line": 375, "path": "apps/ios/Sources/Design/ChatProTab.swift", "source": "Opens Settings / Gateway", "surface": "apple", "id": "native.apple.d19c9577e0fdfea7" }, + { + "kind": "ui-localized-call", + "line": 378, + "path": "apps/ios/Sources/Design/ChatProTab.swift", + "source": "Hides the gateway status label", + "surface": "apple", + "id": "native.apple.b59e690104ae3e24" + }, + { + "kind": "ui-localized-call", + "line": 379, + "path": "apps/ios/Sources/Design/ChatProTab.swift", + "source": "Shows the full gateway status label", + "surface": "apple", + "id": "native.apple.5fcf9d462463dd27" + }, + { + "kind": "ui-localized-call", + "line": 398, + "path": "apps/ios/Sources/Design/ChatProTab.swift", + "source": "Voice off", + "surface": "apple", + "id": "native.apple.592b1e673e42df50" + }, { "kind": "ui-call", - "line": 355, + "line": 539, "path": "apps/ios/Sources/Design/ChatProTab.swift", "source": "New Chat", "surface": "apple", @@ -16843,7 +16947,7 @@ }, { "kind": "ui-call", - "line": 368, + "line": 552, "path": "apps/ios/Sources/Design/ChatProTab.swift", "source": "New Chat in Worktree", "surface": "apple", @@ -16851,7 +16955,7 @@ }, { "kind": "ui-localized-call", - "line": 381, + "line": 565, "path": "apps/ios/Sources/Design/ChatProTab.swift", "source": "Sessions…", "surface": "apple", @@ -16859,7 +16963,7 @@ }, { "kind": "ui-call", - "line": 394, + "line": 578, "path": "apps/ios/Sources/Design/ChatProTab.swift", "source": "Background Tasks", "surface": "apple", @@ -16867,15 +16971,23 @@ }, { "kind": "ui-call", - "line": 406, + "line": 590, "path": "apps/ios/Sources/Design/ChatProTab.swift", "source": "Export Transcript", "surface": "apple", "id": "native.apple.ced228bb4d20a726" }, + { + "kind": "ui-call", + "line": 603, + "path": "apps/ios/Sources/Design/ChatProTab.swift", + "source": "Gateway Settings", + "surface": "apple", + "id": "native.apple.d510e012c91b24ed" + }, { "kind": "ui-localized-call", - "line": 416, + "line": 614, "path": "apps/ios/Sources/Design/ChatProTab.swift", "source": "Show reasoning & tool activity", "surface": "apple", @@ -16883,7 +16995,7 @@ }, { "kind": "ui-modifier", - "line": 425, + "line": 623, "path": "apps/ios/Sources/Design/ChatProTab.swift", "source": "Chat actions", "surface": "apple", @@ -16891,7 +17003,7 @@ }, { "kind": "conditional-branch", - "line": 494, + "line": 717, "path": "apps/ios/Sources/Design/ChatProTab.swift", "source": "Connected", "surface": "apple", @@ -16899,7 +17011,7 @@ }, { "kind": "conditional-branch", - "line": 494, + "line": 717, "path": "apps/ios/Sources/Design/ChatProTab.swift", "source": "Unavailable", "surface": "apple", @@ -16907,7 +17019,7 @@ }, { "kind": "ui-localized-call", - "line": 507, + "line": 730, "path": "apps/ios/Sources/Design/ChatProTab.swift", "source": "Message %@...", "surface": "apple", @@ -16915,7 +17027,7 @@ }, { "kind": "ui-localized-call", - "line": 512, + "line": 735, "path": "apps/ios/Sources/Design/ChatProTab.swift", "source": "Message %@; sends when connected", "surface": "apple", @@ -16923,7 +17035,7 @@ }, { "kind": "ui-localized-call", - "line": 515, + "line": 738, "path": "apps/ios/Sources/Design/ChatProTab.swift", "source": "Connect to a gateway", "surface": "apple", @@ -16931,7 +17043,7 @@ }, { "kind": "ui-localized-call", - "line": 613, + "line": 827, "path": "apps/ios/Sources/Design/ChatProTab.swift", "source": "Check OpenClaw status", "surface": "apple", @@ -16939,7 +17051,7 @@ }, { "kind": "ui-localized-call", - "line": 614, + "line": 828, "path": "apps/ios/Sources/Design/ChatProTab.swift", "source": "Summarize the current OpenClaw status and tell me what needs attention.", "surface": "apple", @@ -16947,7 +17059,7 @@ }, { "kind": "ui-localized-call", - "line": 617, + "line": 831, "path": "apps/ios/Sources/Design/ChatProTab.swift", "source": "What can I control here?", "surface": "apple", @@ -16955,7 +17067,7 @@ }, { "kind": "ui-localized-call", - "line": 618, + "line": 832, "path": "apps/ios/Sources/Design/ChatProTab.swift", "source": "Show me which phone controls and device capabilities are available right now.", "surface": "apple", @@ -16963,7 +17075,7 @@ }, { "kind": "ui-localized-call", - "line": 621, + "line": 835, "path": "apps/ios/Sources/Design/ChatProTab.swift", "source": "Help me start voice chat", "surface": "apple", @@ -16971,7 +17083,7 @@ }, { "kind": "ui-localized-call", - "line": 622, + "line": 836, "path": "apps/ios/Sources/Design/ChatProTab.swift", "source": "Help me start a realtime voice session from this phone.", "surface": "apple", @@ -18691,7 +18803,7 @@ }, { "kind": "ui-modifier", - "line": 626, + "line": 616, "path": "apps/ios/Sources/Design/OpenClawProComponents.swift", "source": "OpenClaw", "surface": "apple", @@ -18699,7 +18811,7 @@ }, { "kind": "ui-localized-call", - "line": 656, + "line": 646, "path": "apps/ios/Sources/Design/OpenClawProComponents.swift", "source": "Gateway %@", "surface": "apple", @@ -18707,7 +18819,7 @@ }, { "kind": "ui-localized-call", - "line": 663, + "line": 653, "path": "apps/ios/Sources/Design/OpenClawProComponents.swift", "source": "Online", "surface": "apple", @@ -18715,7 +18827,7 @@ }, { "kind": "ui-localized-call", - "line": 665, + "line": 655, "path": "apps/ios/Sources/Design/OpenClawProComponents.swift", "source": "Connecting", "surface": "apple", @@ -18723,7 +18835,7 @@ }, { "kind": "ui-localized-call", - "line": 667, + "line": 657, "path": "apps/ios/Sources/Design/OpenClawProComponents.swift", "source": "Attention", "surface": "apple", @@ -18731,7 +18843,7 @@ }, { "kind": "ui-localized-call", - "line": 669, + "line": 659, "path": "apps/ios/Sources/Design/OpenClawProComponents.swift", "source": "Offline", "surface": "apple", @@ -18755,7 +18867,7 @@ }, { "kind": "ui-named-argument", - "line": 100, + "line": 97, "path": "apps/ios/Sources/Design/RootTabsPhoneControlHub.swift", "source": "Agent chat and recent work.", "surface": "apple", @@ -18763,15 +18875,7 @@ }, { "kind": "ui-named-argument", - "line": 103, - "path": "apps/ios/Sources/Design/RootTabsPhoneControlHub.swift", - "source": "Realtime voice and controls.", - "surface": "apple", - "id": "native.apple.a30d0a950d0a77aa" - }, - { - "kind": "ui-named-argument", - "line": 173, + "line": 166, "path": "apps/ios/Sources/Design/RootTabsPhoneControlHub.swift", "source": "Overview", "surface": "apple", @@ -18779,7 +18883,7 @@ }, { "kind": "ui-named-argument", - "line": 195, + "line": 188, "path": "apps/ios/Sources/Design/RootTabsPhoneControlHub.swift", "source": "Instances", "surface": "apple", @@ -18787,7 +18891,7 @@ }, { "kind": "ui-named-argument", - "line": 204, + "line": 197, "path": "apps/ios/Sources/Design/RootTabsPhoneControlHub.swift", "source": "Files", "surface": "apple", @@ -18795,7 +18899,7 @@ }, { "kind": "ui-named-argument", - "line": 209, + "line": 202, "path": "apps/ios/Sources/Design/RootTabsPhoneControlHub.swift", "source": "Dreaming", "surface": "apple", @@ -18803,7 +18907,7 @@ }, { "kind": "ui-named-argument", - "line": 214, + "line": 207, "path": "apps/ios/Sources/Design/RootTabsPhoneControlHub.swift", "source": "Usage", "surface": "apple", @@ -18811,7 +18915,7 @@ }, { "kind": "ui-named-argument", - "line": 219, + "line": 212, "path": "apps/ios/Sources/Design/RootTabsPhoneControlHub.swift", "source": "Automations", "surface": "apple", @@ -18819,7 +18923,7 @@ }, { "kind": "ui-localized-call", - "line": 284, + "line": 277, "path": "apps/ios/Sources/Design/RootTabsPhoneControlHub.swift", "source": "Default Agent", "surface": "apple", @@ -18827,7 +18931,7 @@ }, { "kind": "ui-call", - "line": 299, + "line": 292, "path": "apps/ios/Sources/Design/RootTabsPhoneControlHub.swift", "source": "Gateway", "surface": "apple", @@ -18835,7 +18939,7 @@ }, { "kind": "ui-localized-call", - "line": 307, + "line": 300, "path": "apps/ios/Sources/Design/RootTabsPhoneControlHub.swift", "source": "Gateway %1$@, %2$@, %3$@", "surface": "apple", @@ -18843,7 +18947,7 @@ }, { "kind": "ui-localized-call", - "line": 314, + "line": 307, "path": "apps/ios/Sources/Design/RootTabsPhoneControlHub.swift", "source": "Gateway %1$@, %2$@", "surface": "apple", @@ -18851,7 +18955,7 @@ }, { "kind": "ui-localized-call", - "line": 323, + "line": 316, "path": "apps/ios/Sources/Design/RootTabsPhoneControlHub.swift", "source": "Online", "surface": "apple", @@ -18859,7 +18963,7 @@ }, { "kind": "ui-localized-call", - "line": 324, + "line": 317, "path": "apps/ios/Sources/Design/RootTabsPhoneControlHub.swift", "source": "Connecting", "surface": "apple", @@ -18867,7 +18971,7 @@ }, { "kind": "ui-localized-call", - "line": 325, + "line": 318, "path": "apps/ios/Sources/Design/RootTabsPhoneControlHub.swift", "source": "Attention", "surface": "apple", @@ -18875,7 +18979,7 @@ }, { "kind": "ui-localized-call", - "line": 326, + "line": 319, "path": "apps/ios/Sources/Design/RootTabsPhoneControlHub.swift", "source": "Offline", "surface": "apple", @@ -19587,7 +19691,7 @@ }, { "kind": "ui-localized-call", - "line": 404, + "line": 405, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Opening QR scanner...", "surface": "apple", @@ -19595,7 +19699,7 @@ }, { "kind": "ui-localized-call", - "line": 410, + "line": 411, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "QR loaded. Closing scanner...", "surface": "apple", @@ -19603,7 +19707,7 @@ }, { "kind": "ui-localized-call", - "line": 440, + "line": 441, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Apple Review demo mode enabled.", "surface": "apple", @@ -19611,7 +19715,7 @@ }, { "kind": "ui-localized-call", - "line": 491, + "line": 492, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Failed: host required", "surface": "apple", @@ -19619,7 +19723,7 @@ }, { "kind": "ui-localized-call", - "line": 499, + "line": 500, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Failed: invalid port", "surface": "apple", @@ -19627,7 +19731,7 @@ }, { "kind": "ui-localized-call", - "line": 557, + "line": 558, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Tailscale is off on this device. Turn it on, then try again.", "surface": "apple", @@ -19635,7 +19739,7 @@ }, { "kind": "ui-localized-call", - "line": 934, + "line": 935, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Gateway", "surface": "apple", @@ -19643,7 +19747,7 @@ }, { "kind": "ui-localized-call", - "line": 935, + "line": 936, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Apple Watch", "surface": "apple", @@ -19651,7 +19755,7 @@ }, { "kind": "ui-localized-call", - "line": 936, + "line": 937, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Approvals", "surface": "apple", @@ -19659,7 +19763,7 @@ }, { "kind": "ui-localized-call", - "line": 937, + "line": 938, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Permissions", "surface": "apple", @@ -19667,7 +19771,7 @@ }, { "kind": "ui-localized-call", - "line": 938, + "line": 939, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Channels", "surface": "apple", @@ -19675,7 +19779,7 @@ }, { "kind": "ui-localized-call", - "line": 939, + "line": 940, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Skills", "surface": "apple", @@ -19683,7 +19787,7 @@ }, { "kind": "ui-localized-call", - "line": 940, + "line": 941, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Voice & Talk", "surface": "apple", @@ -19691,7 +19795,7 @@ }, { "kind": "ui-localized-call", - "line": 941, + "line": 942, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Diagnostics", "surface": "apple", @@ -19699,7 +19803,7 @@ }, { "kind": "ui-localized-call", - "line": 942, + "line": 943, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Privacy", "surface": "apple", @@ -19707,7 +19811,7 @@ }, { "kind": "ui-localized-call", - "line": 944, + "line": 945, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Licenses", "surface": "apple", @@ -19715,7 +19819,7 @@ }, { "kind": "ui-localized-call", - "line": 945, + "line": 946, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "About", "surface": "apple", @@ -19723,7 +19827,7 @@ }, { "kind": "ui-localized-call", - "line": 952, + "line": 953, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Preparing one-time setup…", "surface": "apple", @@ -19731,7 +19835,7 @@ }, { "kind": "ui-localized-call", - "line": 957, + "line": 958, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Setup sent. Open OpenClaw on the watch to connect.", "surface": "apple", @@ -19739,7 +19843,7 @@ }, { "kind": "ui-localized-call", - "line": 959, + "line": 960, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Setup queued for the watch. Open OpenClaw before the code expires.", "surface": "apple", @@ -19747,7 +19851,7 @@ }, { "kind": "ui-localized-call", - "line": 1049, + "line": 1050, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "This gateway is on your tailnet. Turn on Tailscale on this device, then tap Connect.", "surface": "apple", @@ -19755,7 +19859,7 @@ }, { "kind": "ui-localized-call", - "line": 1056, + "line": 1057, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Pairing required. Run /pair approve in your OpenClaw chat, then connect again.", "surface": "apple", @@ -19763,7 +19867,7 @@ }, { "kind": "ui-localized-call", - "line": 1059, + "line": 1060, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Secure handshake failed. Check Tailscale, then connect again.", "surface": "apple", @@ -19771,7 +19875,7 @@ }, { "kind": "ui-localized-call", - "line": 1068, + "line": 1069, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Connection timed out. Make sure Tailscale is connected, then try again.", "surface": "apple", @@ -19779,7 +19883,7 @@ }, { "kind": "ui-localized-call", - "line": 1072, + "line": 1073, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Connected, but some controls are restricted for nodes. This is expected.", "surface": "apple", @@ -19787,7 +19891,7 @@ }, { "kind": "ui-localized-call", - "line": 1079, + "line": 1080, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Setup code applied. Connecting...", "surface": "apple", @@ -19795,7 +19899,7 @@ }, { "kind": "ui-localized-call", - "line": 1080, + "line": 1081, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Checking gateway reachability...", "surface": "apple", @@ -19803,7 +19907,7 @@ }, { "kind": "ui-localized-call", - "line": 1081, + "line": 1082, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "QR loaded. Connecting to %@:%@...", "surface": "apple", @@ -19811,7 +19915,7 @@ }, { "kind": "ui-localized-call", - "line": 1144, + "line": 1145, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Not loaded", "surface": "apple", @@ -19819,7 +19923,7 @@ }, { "kind": "ui-localized-call", - "line": 1147, + "line": 1148, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Configured", "surface": "apple", @@ -19827,7 +19931,7 @@ }, { "kind": "ui-localized-call", - "line": 1148, + "line": 1149, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Not configured", "surface": "apple", @@ -19835,7 +19939,7 @@ }, { "kind": "ui-localized-call", - "line": 1155, + "line": 1156, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Not active", "surface": "apple", @@ -19843,7 +19947,7 @@ }, { "kind": "ui-localized-call", - "line": 1191, + "line": 1192, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Apple Review demo mode", "surface": "apple", @@ -19851,7 +19955,7 @@ }, { "kind": "ui-localized-call", - "line": 1194, + "line": 1195, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Connected", "surface": "apple", @@ -19859,7 +19963,7 @@ }, { "kind": "ui-localized-call", - "line": 1200, + "line": 1201, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "offline", "surface": "apple", @@ -19867,7 +19971,7 @@ }, { "kind": "ui-localized-call", - "line": 1200, + "line": 1201, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "online", "surface": "apple", @@ -19875,7 +19979,7 @@ }, { "kind": "ui-localized-call", - "line": 1218, + "line": 1219, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Live gateway requests are disabled in demo mode.", "surface": "apple", @@ -19883,7 +19987,7 @@ }, { "kind": "ui-localized-call", - "line": 1222, + "line": 1223, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Foreground approvals still appear while OpenClaw is connected.", "surface": "apple", @@ -19891,7 +19995,7 @@ }, { "kind": "ui-localized-call", - "line": 1225, + "line": 1226, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Gateway requests will appear here.", "surface": "apple", @@ -19899,7 +20003,7 @@ }, { "kind": "ui-localized-call", - "line": 1226, + "line": 1227, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Connect to the gateway.", "surface": "apple", @@ -19907,7 +20011,7 @@ }, { "kind": "ui-localized-call", - "line": 1230, + "line": 1231, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Demo mode only", "surface": "apple", @@ -19915,7 +20019,7 @@ }, { "kind": "ui-localized-call", - "line": 1237, + "line": 1238, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "loaded", "surface": "apple", @@ -19923,7 +20027,7 @@ }, { "kind": "ui-localized-call", - "line": 1238, + "line": 1239, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "missing", "surface": "apple", @@ -19931,7 +20035,7 @@ }, { "kind": "ui-localized-call", - "line": 1247, + "line": 1248, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Waiting for gateway", "surface": "apple", @@ -19939,7 +20043,7 @@ }, { "kind": "ui-localized-call", - "line": 1264, + "line": 1265, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "1 waiting", "surface": "apple", @@ -19947,7 +20051,7 @@ }, { "kind": "ui-localized-call", - "line": 1267, + "line": 1268, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "%@ waiting", "surface": "apple", @@ -19955,7 +20059,7 @@ }, { "kind": "ui-localized-call", - "line": 1278, + "line": 1279, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Review gateway action", "surface": "apple", @@ -19963,7 +20067,7 @@ }, { "kind": "ui-localized-call", - "line": 1280, + "line": 1281, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Agent: %@", "surface": "apple", @@ -19971,7 +20075,7 @@ }, { "kind": "ui-localized-call", - "line": 1289, + "line": 1290, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Resolving", "surface": "apple", @@ -19979,7 +20083,7 @@ }, { "kind": "ui-localized-call", - "line": 1290, + "line": 1291, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "High", "surface": "apple", @@ -19987,7 +20091,7 @@ }, { "kind": "ui-localized-call", - "line": 1296, + "line": 1297, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Permission can be saved", "surface": "apple", @@ -19995,7 +20099,7 @@ }, { "kind": "ui-localized-call", - "line": 1297, + "line": 1298, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "One-time approval", "surface": "apple", @@ -20003,7 +20107,7 @@ }, { "kind": "ui-localized-call", - "line": 1300, + "line": 1301, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Medium", "surface": "apple", @@ -20011,7 +20115,7 @@ }, { "kind": "ui-localized-call", - "line": 1301, + "line": 1302, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Review", "surface": "apple", @@ -20019,7 +20123,7 @@ }, { "kind": "ui-localized-call", - "line": 1307, + "line": 1308, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Talk + Wake", "surface": "apple", @@ -20027,7 +20131,7 @@ }, { "kind": "ui-localized-call", - "line": 1308, + "line": 1309, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Talk on", "surface": "apple", @@ -20035,7 +20139,7 @@ }, { "kind": "ui-localized-call", - "line": 1309, + "line": 1310, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Wake on", "surface": "apple", @@ -20043,7 +20147,7 @@ }, { "kind": "ui-localized-call", - "line": 1310, + "line": 1311, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Off", "surface": "apple", @@ -20051,7 +20155,7 @@ }, { "kind": "ui-localized-call", - "line": 1314, + "line": 1315, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "demo", "surface": "apple", @@ -20059,7 +20163,7 @@ }, { "kind": "ui-localized-call", - "line": 1315, + "line": 1316, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "ready", "surface": "apple", @@ -20067,7 +20171,7 @@ }, { "kind": "ui-localized-call", - "line": 1316, + "line": 1317, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "check", "surface": "apple", @@ -20075,7 +20179,7 @@ }, { "kind": "ui-localized-call", - "line": 1317, + "line": 1318, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "partial", "surface": "apple", @@ -20083,7 +20187,7 @@ }, { "kind": "ui-localized-call", - "line": 1321, + "line": 1322, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "pending", "surface": "apple", @@ -20091,7 +20195,7 @@ }, { "kind": "ui-localized-call", - "line": 1323, + "line": 1324, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "pass", "surface": "apple", @@ -20099,7 +20203,7 @@ }, { "kind": "ui-localized-call", - "line": 1334, + "line": 1335, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Requesting iOS location permission…", "surface": "apple", @@ -20107,7 +20211,7 @@ }, { "kind": "ui-localized-call", - "line": 1400, + "line": 1401, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "This build uses OpenClaw's hosted push relay at %@ for notification delivery data.", "surface": "apple", @@ -20115,7 +20219,7 @@ }, { "kind": "ui-localized-call", - "line": 1404, + "line": 1405, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "This build is not configured to use OpenClaw's hosted push relay.", "surface": "apple", @@ -20123,7 +20227,7 @@ }, { "kind": "ui-localized-call", - "line": 1409, + "line": 1410, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Enabling this sends delivery data through OpenClaw's hosted push relay.", "surface": "apple", @@ -22105,238 +22209,6 @@ "surface": "apple", "id": "native.apple.7a5a1b9342f01954" }, - { - "kind": "ui-modifier", - "line": 62, - "path": "apps/ios/Sources/Design/TalkProTab.swift", - "source": "Enable Talk", - "surface": "apple", - "id": "native.apple.798c3584ad95f8da" - }, - { - "kind": "ui-call", - "line": 68, - "path": "apps/ios/Sources/Design/TalkProTab.swift", - "source": "Not Now", - "surface": "apple", - "id": "native.apple.c6e171128190d131" - }, - { - "kind": "ui-modifier", - "line": 107, - "path": "apps/ios/Sources/Design/TalkProTab.swift", - "source": "Talk", - "surface": "apple", - "id": "native.apple.e4b15aef3c2e3da7" - }, - { - "kind": "ui-call", - "line": 161, - "path": "apps/ios/Sources/Design/TalkProTab.swift", - "source": "Conversation", - "surface": "apple", - "id": "native.apple.ab3381742bdff650" - }, - { - "kind": "ui-call", - "line": 162, - "path": "apps/ios/Sources/Design/TalkProTab.swift", - "source": "Agent", - "surface": "apple", - "id": "native.apple.7af100f6640c6a69" - }, - { - "kind": "ui-call", - "line": 163, - "path": "apps/ios/Sources/Design/TalkProTab.swift", - "source": "Session", - "surface": "apple", - "id": "native.apple.49c4de60117b1a98" - }, - { - "kind": "ui-call", - "line": 164, - "path": "apps/ios/Sources/Design/TalkProTab.swift", - "source": "Runtime", - "surface": "apple", - "id": "native.apple.37c29d2506b955e9" - }, - { - "kind": "ui-call", - "line": 169, - "path": "apps/ios/Sources/Design/TalkProTab.swift", - "source": "Voice Mode", - "surface": "apple", - "id": "native.apple.13e0ad79b141576e" - }, - { - "kind": "ui-call", - "line": 170, - "path": "apps/ios/Sources/Design/TalkProTab.swift", - "source": "Configured", - "surface": "apple", - "id": "native.apple.e8035b71bab833c7" - }, - { - "kind": "ui-call", - "line": 173, - "path": "apps/ios/Sources/Design/TalkProTab.swift", - "source": "Active", - "surface": "apple", - "id": "native.apple.1b2845ec89d20eef" - }, - { - "kind": "ui-call", - "line": 174, - "path": "apps/ios/Sources/Design/TalkProTab.swift", - "source": "Transport", - "surface": "apple", - "id": "native.apple.a82c07df64d13b23" - }, - { - "kind": "ui-call", - "line": 176, - "path": "apps/ios/Sources/Design/TalkProTab.swift", - "source": "Last issue", - "surface": "apple", - "id": "native.apple.384e097278f76954" - }, - { - "kind": "ui-call", - "line": 178, - "path": "apps/ios/Sources/Design/TalkProTab.swift", - "source": "Permission", - "surface": "apple", - "id": "native.apple.a6d8fc7bfd98836f" - }, - { - "kind": "ui-call", - "line": 179, - "path": "apps/ios/Sources/Design/TalkProTab.swift", - "source": "Speech language", - "surface": "apple", - "id": "native.apple.983bdcd5e3f1baa8" - }, - { - "kind": "ui-call", - "line": 184, - "path": "apps/ios/Sources/Design/TalkProTab.swift", - "source": "Controls", - "surface": "apple", - "id": "native.apple.32e7fa2b1798e9c3" - }, - { - "kind": "ui-call", - "line": 186, - "path": "apps/ios/Sources/Design/TalkProTab.swift", - "source": "Speakerphone", - "surface": "apple", - "id": "native.apple.0eb012a5d3d81b32" - }, - { - "kind": "ui-call", - "line": 191, - "path": "apps/ios/Sources/Design/TalkProTab.swift", - "source": "Background listening", - "surface": "apple", - "id": "native.apple.ef8699e2718346da" - }, - { - "kind": "ui-call", - "line": 197, - "path": "apps/ios/Sources/Design/TalkProTab.swift", - "source": "Voice & Talk Settings", - "surface": "apple", - "id": "native.apple.1e6938a09b4fbe77" - }, - { - "kind": "ui-call", - "line": 225, - "path": "apps/ios/Sources/Design/TalkProTab.swift", - "source": "Gateway approval is required before this phone can capture voice.", - "surface": "apple", - "id": "native.apple.c7a591fd5bb6687e" - }, - { - "kind": "ui-call", - "line": 228, - "path": "apps/ios/Sources/Design/TalkProTab.swift", - "source": "Voice is disabled in Apple Review demo mode.", - "surface": "apple", - "id": "native.apple.9d40e75e55ed39eb" - }, - { - "kind": "ui-call", - "line": 231, - "path": "apps/ios/Sources/Design/TalkProTab.swift", - "source": "Connect to your gateway to start a voice conversation.", - "surface": "apple", - "id": "native.apple.3ef69bf916177799" - }, - { - "kind": "ui-call", - "line": 234, - "path": "apps/ios/Sources/Design/TalkProTab.swift", - "source": "Open Voice settings after the gateway loads Talk configuration.", - "surface": "apple", - "id": "native.apple.ed03af7c68841a5d" - }, - { - "kind": "ui-localized-call", - "line": 241, - "path": "apps/ios/Sources/Design/TalkProTab.swift", - "source": "Routes voice to %@.", - "surface": "apple", - "id": "native.apple.883a251978601b96" - }, - { - "kind": "conditional-branch", - "line": 254, - "path": "apps/ios/Sources/Design/TalkProTab.swift", - "source": "Not loaded", - "surface": "apple", - "id": "native.apple.877fb5c1abfaafa1" - }, - { - "kind": "ui-localized-call", - "line": 263, - "path": "apps/ios/Sources/Design/TalkProTab.swift", - "source": "Not active", - "surface": "apple", - "id": "native.apple.d3ccb092936551fc" - }, - { - "kind": "conditional-branch", - "line": 441, - "path": "apps/ios/Sources/Design/TalkProTab.swift", - "source": "Open Gateway Settings", - "surface": "apple", - "id": "native.apple.1620f7cb3deea133" - }, - { - "kind": "conditional-branch", - "line": 441, - "path": "apps/ios/Sources/Design/TalkProTab.swift", - "source": "Open Voice Settings", - "surface": "apple", - "id": "native.apple.3fd2706ef0f14fd2" - }, - { - "kind": "conditional-branch", - "line": 442, - "path": "apps/ios/Sources/Design/TalkProTab.swift", - "source": "Demo Mode Only", - "surface": "apple", - "id": "native.apple.3cc9cd23a67db077" - }, - { - "kind": "conditional-branch", - "line": 442, - "path": "apps/ios/Sources/Design/TalkProTab.swift", - "source": "Waiting for Approval", - "surface": "apple", - "id": "native.apple.12c9d8231777366c" - }, { "kind": "ui-named-argument", "line": 17, @@ -23211,7 +23083,7 @@ }, { "kind": "ui-localized-call", - "line": 169, + "line": 565, "path": "apps/ios/Sources/LiveActivity/LiveActivityManager.swift", "source": "Connecting...", "surface": "apple", @@ -23219,7 +23091,7 @@ }, { "kind": "ui-localized-call", - "line": 172, + "line": 568, "path": "apps/ios/Sources/LiveActivity/LiveActivityManager.swift", "source": "Reconnecting...", "surface": "apple", @@ -23227,7 +23099,7 @@ }, { "kind": "ui-localized-call", - "line": 179, + "line": 575, "path": "apps/ios/Sources/LiveActivity/LiveActivityManager.swift", "source": "Approval needed", "surface": "apple", @@ -23235,12 +23107,68 @@ }, { "kind": "ui-localized-call", - "line": 182, + "line": 578, "path": "apps/ios/Sources/LiveActivity/LiveActivityManager.swift", "source": "Action required", "surface": "apple", "id": "native.apple.8faaac6dab276c0a" }, + { + "kind": "ui-localized-call", + "line": 596, + "path": "apps/ios/Sources/LiveActivity/LiveActivityManager.swift", + "source": "Listening", + "surface": "apple", + "id": "native.apple.7205f0822706cb55" + }, + { + "kind": "ui-localized-call", + "line": 597, + "path": "apps/ios/Sources/LiveActivity/LiveActivityManager.swift", + "source": "Listening (PTT)", + "surface": "apple", + "id": "native.apple.8b10cb1dc21e42af" + }, + { + "kind": "ui-localized-call", + "line": 598, + "path": "apps/ios/Sources/LiveActivity/LiveActivityManager.swift", + "source": "Listening (Realtime)", + "surface": "apple", + "id": "native.apple.939c7ec55636c67f" + }, + { + "kind": "ui-localized-call", + "line": 602, + "path": "apps/ios/Sources/LiveActivity/LiveActivityManager.swift", + "source": "Speaking…", + "surface": "apple", + "id": "native.apple.86f203ebb2453d15" + }, + { + "kind": "ui-localized-call", + "line": 603, + "path": "apps/ios/Sources/LiveActivity/LiveActivityManager.swift", + "source": "Speaking (System)…", + "surface": "apple", + "id": "native.apple.1e3ff6b56ae3828d" + }, + { + "kind": "ui-localized-call", + "line": 604, + "path": "apps/ios/Sources/LiveActivity/LiveActivityManager.swift", + "source": "Generating voice…", + "surface": "apple", + "id": "native.apple.37a5fedaefb8ebf3" + }, + { + "kind": "ui-localized-call", + "line": 607, + "path": "apps/ios/Sources/LiveActivity/LiveActivityManager.swift", + "source": "Ready", + "surface": "apple", + "id": "native.apple.47003f094346c660" + }, { "kind": "conditional-branch", "line": 43, @@ -23475,7 +23403,7 @@ }, { "kind": "ui-localized-call", - "line": 3864, + "line": 4065, "path": "apps/ios/Sources/Model/NodeAppModel.swift", "source": "Approval needed", "surface": "apple", @@ -23483,7 +23411,7 @@ }, { "kind": "ui-localized-call", - "line": 3865, + "line": 4066, "path": "apps/ios/Sources/Model/NodeAppModel.swift", "source": "Action required", "surface": "apple", @@ -23491,7 +23419,7 @@ }, { "kind": "ui-localized-call", - "line": 4564, + "line": 4781, "path": "apps/ios/Sources/Model/NodeAppModel.swift", "source": "Connecting...", "surface": "apple", @@ -23499,7 +23427,7 @@ }, { "kind": "ui-localized-call", - "line": 4565, + "line": 4782, "path": "apps/ios/Sources/Model/NodeAppModel.swift", "source": "Reconnecting...", "surface": "apple", @@ -23507,7 +23435,7 @@ }, { "kind": "conditional-branch", - "line": 4917, + "line": 5134, "path": "apps/ios/Sources/Model/NodeAppModel.swift", "source": "Connected", "surface": "apple", @@ -23515,7 +23443,7 @@ }, { "kind": "conditional-branch", - "line": 4917, + "line": 5134, "path": "apps/ios/Sources/Model/NodeAppModel.swift", "source": "Offline", "surface": "apple", @@ -23523,7 +23451,7 @@ }, { "kind": "conditional-branch", - "line": 6087, + "line": 6304, "path": "apps/ios/Sources/Model/NodeAppModel.swift", "source": "No chat messages yet", "surface": "apple", @@ -23531,7 +23459,7 @@ }, { "kind": "conditional-branch", - "line": 6286, + "line": 6503, "path": "apps/ios/Sources/Model/NodeAppModel.swift", "source": "Connecting…", "surface": "apple", @@ -23539,7 +23467,7 @@ }, { "kind": "conditional-branch", - "line": 6286, + "line": 6503, "path": "apps/ios/Sources/Model/NodeAppModel.swift", "source": "Reconnecting…", "surface": "apple", @@ -23547,7 +23475,7 @@ }, { "kind": "conditional-branch", - "line": 8695, + "line": 8912, "path": "apps/ios/Sources/Model/NodeAppModel.swift", "source": "Approval", "surface": "apple", @@ -23555,7 +23483,7 @@ }, { "kind": "conditional-branch", - "line": 8695, + "line": 8912, "path": "apps/ios/Sources/Model/NodeAppModel.swift", "source": "This approval was already", "surface": "apple", @@ -23563,7 +23491,7 @@ }, { "kind": "conditional-branch", - "line": 8701, + "line": 8918, "path": "apps/ios/Sources/Model/NodeAppModel.swift", "source": "This approval was already set to Always Allow.", "surface": "apple", @@ -23571,7 +23499,7 @@ }, { "kind": "conditional-branch", - "line": 8702, + "line": 8919, "path": "apps/ios/Sources/Model/NodeAppModel.swift", "source": "Approval set to Always Allow.", "surface": "apple", @@ -23579,7 +23507,7 @@ }, { "kind": "conditional-branch", - "line": 10040, + "line": 10257, "path": "apps/ios/Sources/Model/NodeAppModel.swift", "source": "\\(urlText.prefix(500))…", "surface": "apple", @@ -24555,39 +24483,7 @@ }, { "kind": "ui-call", - "line": 174, - "path": "apps/ios/Sources/RootTabs.swift", - "source": "Talk", - "surface": "apple", - "id": "native.apple.f2ea327bfea8b8e3" - }, - { - "kind": "ui-call", - "line": 187, - "path": "apps/ios/Sources/RootTabs.swift", - "source": "Control", - "surface": "apple", - "id": "native.apple.bda6cfa988e49a75" - }, - { - "kind": "ui-call", - "line": 196, - "path": "apps/ios/Sources/RootTabs.swift", - "source": "Agent", - "surface": "apple", - "id": "native.apple.3c80f5351055a5f3" - }, - { - "kind": "ui-call", - "line": 209, - "path": "apps/ios/Sources/RootTabs.swift", - "source": "Settings", - "surface": "apple", - "id": "native.apple.22d107db5b905ef4" - }, - { - "kind": "ui-call", - "line": 308, + "line": 249, "path": "apps/ios/Sources/RootTabs.swift", "source": "OpenClaw", "surface": "apple", @@ -24595,7 +24491,7 @@ }, { "kind": "ui-localized-call", - "line": 340, + "line": 281, "path": "apps/ios/Sources/RootTabs.swift", "source": "OpenClaw %@", "surface": "apple", @@ -24603,7 +24499,7 @@ }, { "kind": "ui-localized-call", - "line": 347, + "line": 288, "path": "apps/ios/Sources/RootTabs.swift", "source": "Online", "surface": "apple", @@ -24611,7 +24507,7 @@ }, { "kind": "ui-localized-call", - "line": 349, + "line": 290, "path": "apps/ios/Sources/RootTabs.swift", "source": "Connecting", "surface": "apple", @@ -24619,7 +24515,7 @@ }, { "kind": "ui-localized-call", - "line": 351, + "line": 292, "path": "apps/ios/Sources/RootTabs.swift", "source": "Needs attention", "surface": "apple", @@ -24627,7 +24523,7 @@ }, { "kind": "ui-localized-call", - "line": 353, + "line": 294, "path": "apps/ios/Sources/RootTabs.swift", "source": "Offline", "surface": "apple", @@ -24635,7 +24531,7 @@ }, { "kind": "ui-named-argument", - "line": 432, + "line": 373, "path": "apps/ios/Sources/RootTabs.swift", "source": "Chat", "surface": "apple", @@ -24643,7 +24539,7 @@ }, { "kind": "ui-named-argument", - "line": 445, + "line": 380, "path": "apps/ios/Sources/RootTabs.swift", "source": "Overview", "surface": "apple", @@ -24651,7 +24547,7 @@ }, { "kind": "ui-named-argument", - "line": 469, + "line": 404, "path": "apps/ios/Sources/RootTabs.swift", "source": "Agents", "surface": "apple", @@ -24659,7 +24555,7 @@ }, { "kind": "ui-named-argument", - "line": 476, + "line": 411, "path": "apps/ios/Sources/RootTabs.swift", "source": "Instances", "surface": "apple", @@ -24667,7 +24563,7 @@ }, { "kind": "ui-named-argument", - "line": 487, + "line": 422, "path": "apps/ios/Sources/RootTabs.swift", "source": "Files", "surface": "apple", @@ -24675,7 +24571,7 @@ }, { "kind": "ui-named-argument", - "line": 494, + "line": 429, "path": "apps/ios/Sources/RootTabs.swift", "source": "Dreaming", "surface": "apple", @@ -24683,7 +24579,7 @@ }, { "kind": "ui-named-argument", - "line": 501, + "line": 436, "path": "apps/ios/Sources/RootTabs.swift", "source": "Usage", "surface": "apple", @@ -24691,7 +24587,7 @@ }, { "kind": "ui-named-argument", - "line": 508, + "line": 443, "path": "apps/ios/Sources/RootTabs.swift", "source": "Automations", "surface": "apple", @@ -24699,7 +24595,7 @@ }, { "kind": "ui-localized-call", - "line": 620, + "line": 555, "path": "apps/ios/Sources/RootTabs.swift", "source": "Show Sidebar", "surface": "apple", @@ -24707,7 +24603,7 @@ }, { "kind": "ui-localized-call", - "line": 631, + "line": 566, "path": "apps/ios/Sources/RootTabs.swift", "source": "Back to %@", "surface": "apple", @@ -24715,7 +24611,7 @@ }, { "kind": "ui-modifier", - "line": 656, + "line": 591, "path": "apps/ios/Sources/RootTabs.swift", "source": "Hide Sidebar", "surface": "apple", @@ -24723,12 +24619,44 @@ }, { "kind": "ui-modifier", - "line": 774, + "line": 709, "path": "apps/ios/Sources/RootTabs.swift", "source": "Close canvas", "surface": "apple", "id": "native.apple.8af43f59602ec0d8" }, + { + "kind": "conditional-branch", + "line": 916, + "path": "apps/ios/Sources/RootTabs.swift", + "source": "Chat, voice active", + "surface": "apple", + "id": "native.apple.a1db6ce0aed1d18e" + }, + { + "kind": "ui-call", + "line": 926, + "path": "apps/ios/Sources/RootTabs.swift", + "source": "Control", + "surface": "apple", + "id": "native.apple.bda6cfa988e49a75" + }, + { + "kind": "ui-call", + "line": 935, + "path": "apps/ios/Sources/RootTabs.swift", + "source": "Agent", + "surface": "apple", + "id": "native.apple.3c80f5351055a5f3" + }, + { + "kind": "ui-call", + "line": 948, + "path": "apps/ios/Sources/RootTabs.swift", + "source": "Settings", + "surface": "apple", + "id": "native.apple.22d107db5b905ef4" + }, { "kind": "conditional-branch", "line": 1026, @@ -24771,7 +24699,7 @@ }, { "kind": "ui-localized-call", - "line": 64, + "line": 62, "path": "apps/ios/Sources/RootTabsNavigation.swift", "source": "Chat", "surface": "apple", @@ -24779,15 +24707,7 @@ }, { "kind": "ui-localized-call", - "line": 65, - "path": "apps/ios/Sources/RootTabsNavigation.swift", - "source": "Talk", - "surface": "apple", - "id": "native.apple.febca7528af94342" - }, - { - "kind": "ui-localized-call", - "line": 66, + "line": 63, "path": "apps/ios/Sources/RootTabsNavigation.swift", "source": "Overview", "surface": "apple", @@ -24795,7 +24715,7 @@ }, { "kind": "ui-localized-call", - "line": 67, + "line": 64, "path": "apps/ios/Sources/RootTabsNavigation.swift", "source": "Activity", "surface": "apple", @@ -24803,7 +24723,7 @@ }, { "kind": "ui-localized-call", - "line": 68, + "line": 65, "path": "apps/ios/Sources/RootTabsNavigation.swift", "source": "Agents", "surface": "apple", @@ -24811,7 +24731,7 @@ }, { "kind": "ui-localized-call", - "line": 69, + "line": 66, "path": "apps/ios/Sources/RootTabsNavigation.swift", "source": "Workboard", "surface": "apple", @@ -24819,7 +24739,7 @@ }, { "kind": "ui-localized-call", - "line": 70, + "line": 67, "path": "apps/ios/Sources/RootTabsNavigation.swift", "source": "Skill Workshop", "surface": "apple", @@ -24827,7 +24747,7 @@ }, { "kind": "ui-localized-call", - "line": 71, + "line": 68, "path": "apps/ios/Sources/RootTabsNavigation.swift", "source": "Instances", "surface": "apple", @@ -24835,7 +24755,7 @@ }, { "kind": "ui-localized-call", - "line": 72, + "line": 69, "path": "apps/ios/Sources/RootTabsNavigation.swift", "source": "Sessions", "surface": "apple", @@ -24843,7 +24763,7 @@ }, { "kind": "ui-localized-call", - "line": 73, + "line": 70, "path": "apps/ios/Sources/RootTabsNavigation.swift", "source": "Files", "surface": "apple", @@ -24851,7 +24771,7 @@ }, { "kind": "ui-localized-call", - "line": 74, + "line": 71, "path": "apps/ios/Sources/RootTabsNavigation.swift", "source": "Dreaming", "surface": "apple", @@ -24859,7 +24779,7 @@ }, { "kind": "ui-localized-call", - "line": 75, + "line": 72, "path": "apps/ios/Sources/RootTabsNavigation.swift", "source": "Usage", "surface": "apple", @@ -24867,7 +24787,7 @@ }, { "kind": "ui-localized-call", - "line": 76, + "line": 73, "path": "apps/ios/Sources/RootTabsNavigation.swift", "source": "Automations", "surface": "apple", @@ -24875,7 +24795,7 @@ }, { "kind": "ui-localized-call", - "line": 77, + "line": 74, "path": "apps/ios/Sources/RootTabsNavigation.swift", "source": "Terminal", "surface": "apple", @@ -24883,7 +24803,7 @@ }, { "kind": "ui-localized-call", - "line": 78, + "line": 75, "path": "apps/ios/Sources/RootTabsNavigation.swift", "source": "Docs", "surface": "apple", @@ -24891,7 +24811,7 @@ }, { "kind": "ui-localized-call", - "line": 79, + "line": 76, "path": "apps/ios/Sources/RootTabsNavigation.swift", "source": "Settings", "surface": "apple", @@ -24899,7 +24819,7 @@ }, { "kind": "ui-localized-call", - "line": 80, + "line": 77, "path": "apps/ios/Sources/RootTabsNavigation.swift", "source": "Settings / Gateway", "surface": "apple", @@ -24907,7 +24827,7 @@ }, { "kind": "ui-localized-call", - "line": 86, + "line": 83, "path": "apps/ios/Sources/RootTabsNavigation.swift", "source": "Connection", "surface": "apple", @@ -25331,7 +25251,7 @@ }, { "kind": "ui-localized-call", - "line": 581, + "line": 591, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "Gateway relay ready", "surface": "apple", @@ -25339,7 +25259,7 @@ }, { "kind": "ui-localized-call", - "line": 583, + "line": 593, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "Realtime Voice, Gateway relay ready", "surface": "apple", @@ -25347,7 +25267,7 @@ }, { "kind": "ui-localized-call", - "line": 585, + "line": 595, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "Listening starts from this phone", "surface": "apple", @@ -25355,7 +25275,7 @@ }, { "kind": "ui-localized-call", - "line": 825, + "line": 835, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "Off", "surface": "apple", @@ -25363,7 +25283,7 @@ }, { "kind": "ui-localized-call", - "line": 828, + "line": 838, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "Not active", "surface": "apple", @@ -25371,7 +25291,7 @@ }, { "kind": "ui-localized-call", - "line": 1005, + "line": 1024, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "Requesting permissions…", "surface": "apple", @@ -25379,23 +25299,7 @@ }, { "kind": "ui-localized-call", - "line": 1013, - "path": "apps/ios/Sources/Voice/TalkModeManager.swift", - "source": "Microphone permission denied", - "surface": "apple", - "id": "native.apple.a1910b5c2e008b6c" - }, - { - "kind": "ui-localized-call", - "line": 1022, - "path": "apps/ios/Sources/Voice/TalkModeManager.swift", - "source": "Speech recognition", - "surface": "apple", - "id": "native.apple.6a48b93f46ea3b08" - }, - { - "kind": "ui-localized-call", - "line": 1058, + "line": 1053, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "Start failed: %@", "surface": "apple", @@ -25403,7 +25307,23 @@ }, { "kind": "ui-localized-call", - "line": 1373, + "line": 1309, + "path": "apps/ios/Sources/Voice/TalkModeManager.swift", + "source": "Microphone permission denied", + "surface": "apple", + "id": "native.apple.a1910b5c2e008b6c" + }, + { + "kind": "ui-localized-call", + "line": 1319, + "path": "apps/ios/Sources/Voice/TalkModeManager.swift", + "source": "Speech recognition", + "surface": "apple", + "id": "native.apple.6a48b93f46ea3b08" + }, + { + "kind": "ui-localized-call", + "line": 1450, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "Offline", "surface": "apple", @@ -25411,7 +25331,7 @@ }, { "kind": "ui-localized-call", - "line": 1572, + "line": 1649, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "Speech error: %@", "surface": "apple", @@ -25419,7 +25339,7 @@ }, { "kind": "ui-localized-call", - "line": 1881, + "line": 1964, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "Gateway not connected", "surface": "apple", @@ -25427,7 +25347,7 @@ }, { "kind": "conditional-branch", - "line": 1925, + "line": 2008, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "Aborted", "surface": "apple", @@ -25435,7 +25355,7 @@ }, { "kind": "conditional-branch", - "line": 1925, + "line": 2008, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "Chat error", "surface": "apple", @@ -25443,7 +25363,7 @@ }, { "kind": "ui-localized-call", - "line": 1953, + "line": 2036, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "Talk failed: %@", "surface": "apple", @@ -25451,7 +25371,7 @@ }, { "kind": "ui-localized-call", - "line": 2057, + "line": 2140, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "No reply", "surface": "apple", @@ -25459,7 +25379,7 @@ }, { "kind": "ui-localized-call", - "line": 2149, + "line": 2232, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "Paused", "surface": "apple", @@ -25467,7 +25387,7 @@ }, { "kind": "ui-localized-call", - "line": 2559, + "line": 2644, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "Generating voice…", "surface": "apple", @@ -25475,7 +25395,7 @@ }, { "kind": "ui-localized-call", - "line": 2697, + "line": 2782, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "Speak failed: %@", "surface": "apple", @@ -25483,7 +25403,7 @@ }, { "kind": "ui-localized-call", - "line": 2796, + "line": 2881, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "Speaking (System)…", "surface": "apple", @@ -25491,7 +25411,7 @@ }, { "kind": "ui-localized-call", - "line": 3669, + "line": 3762, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "iOS System Voice", "surface": "apple", @@ -25499,7 +25419,7 @@ }, { "kind": "ui-localized-call", - "line": 3671, + "line": 3764, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "Realtime Voice", "surface": "apple", @@ -25507,7 +25427,7 @@ }, { "kind": "ui-localized-call", - "line": 3675, + "line": 3768, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "Gateway Default", "surface": "apple", @@ -25515,7 +25435,7 @@ }, { "kind": "ui-localized-call", - "line": 3734, + "line": 3827, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "Listening (Realtime)", "surface": "apple", @@ -25523,7 +25443,7 @@ }, { "kind": "ui-localized-call", - "line": 3736, + "line": 3829, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "Thinking", "surface": "apple", @@ -25531,7 +25451,7 @@ }, { "kind": "ui-localized-call", - "line": 3738, + "line": 3831, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "Thinking…", "surface": "apple", @@ -25539,7 +25459,7 @@ }, { "kind": "ui-localized-call", - "line": 3740, + "line": 3833, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "Asking OpenClaw", "surface": "apple", @@ -25547,7 +25467,7 @@ }, { "kind": "ui-localized-call", - "line": 3742, + "line": 3835, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "Still asking OpenClaw", "surface": "apple", @@ -25555,7 +25475,7 @@ }, { "kind": "ui-localized-call", - "line": 3744, + "line": 3837, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "Updating OpenClaw", "surface": "apple", @@ -25563,7 +25483,7 @@ }, { "kind": "ui-localized-call", - "line": 3746, + "line": 3839, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "Speaking", "surface": "apple", @@ -25571,7 +25491,7 @@ }, { "kind": "ui-localized-call", - "line": 3748, + "line": 3841, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "Speaking…", "surface": "apple", @@ -25579,7 +25499,7 @@ }, { "kind": "ui-localized-call", - "line": 3750, + "line": 3843, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "Connecting", "surface": "apple", @@ -25587,7 +25507,7 @@ }, { "kind": "ui-localized-call", - "line": 3752, + "line": 3845, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "Connecting realtime…", "surface": "apple", @@ -25595,7 +25515,7 @@ }, { "kind": "ui-localized-call", - "line": 3754, + "line": 3847, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "Waiting for realtime…", "surface": "apple", @@ -25603,7 +25523,7 @@ }, { "kind": "ui-localized-call", - "line": 3756, + "line": 3849, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "Ready", "surface": "apple", @@ -25611,7 +25531,7 @@ }, { "kind": "ui-localized-call", - "line": 3758, + "line": 3851, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "Reconnecting", "surface": "apple", @@ -25619,7 +25539,7 @@ }, { "kind": "ui-localized-call", - "line": 3760, + "line": 3853, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "Reconnecting…", "surface": "apple", @@ -25627,7 +25547,7 @@ }, { "kind": "ui-localized-call", - "line": 3762, + "line": 3855, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "Realtime failed before connecting", "surface": "apple", @@ -25635,7 +25555,7 @@ }, { "kind": "ui-localized-call", - "line": 3764, + "line": 3857, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "Realtime disconnected", "surface": "apple", @@ -25643,7 +25563,7 @@ }, { "kind": "ui-localized-call", - "line": 3766, + "line": 3859, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "OpenClaw unavailable", "surface": "apple", @@ -25651,7 +25571,7 @@ }, { "kind": "ui-localized-call", - "line": 3806, + "line": 3899, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "iOS Speech + TTS", "surface": "apple", @@ -25659,7 +25579,7 @@ }, { "kind": "ui-localized-call", - "line": 3808, + "line": 3901, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "Listening", "surface": "apple", @@ -25667,7 +25587,7 @@ }, { "kind": "ui-localized-call", - "line": 3812, + "line": 3905, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "iOS Speech fallback", "surface": "apple", @@ -25675,7 +25595,7 @@ }, { "kind": "ui-localized-call", - "line": 4073, + "line": 4166, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "Gateway Relay", "surface": "apple", @@ -25683,7 +25603,7 @@ }, { "kind": "ui-localized-call", - "line": 4075, + "line": 4168, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "Native WebRTC", "surface": "apple", @@ -25691,7 +25611,7 @@ }, { "kind": "ui-localized-call", - "line": 4076, + "line": 4169, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "Native", "surface": "apple", @@ -25699,7 +25619,7 @@ }, { "kind": "ui-localized-call", - "line": 4137, + "line": 4230, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "Gateway permission required", "surface": "apple", @@ -25707,7 +25627,7 @@ }, { "kind": "ui-localized-call", - "line": 4159, + "line": 4252, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "Not loaded", "surface": "apple", @@ -25715,7 +25635,7 @@ }, { "kind": "ui-localized-call", - "line": 4177, + "line": 4270, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "Approval requested", "surface": "apple", @@ -25723,7 +25643,7 @@ }, { "kind": "ui-localized-call", - "line": 4603, + "line": 4700, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "Realtime unavailable", "surface": "apple", @@ -25731,108 +25651,12 @@ }, { "kind": "ui-localized-call", - "line": 4625, + "line": 4722, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "Listening (PTT)", "surface": "apple", "id": "native.apple.2970f0510ac6b396" }, - { - "kind": "ui-call", - "line": 65, - "path": "apps/ios/Sources/Voice/TalkPermissionPromptView.swift", - "source": "Request ID", - "surface": "apple", - "id": "native.apple.0189269b6a863dc2" - }, - { - "kind": "ui-call", - "line": 76, - "path": "apps/ios/Sources/Voice/TalkPermissionPromptView.swift", - "source": "Sending...", - "surface": "apple", - "id": "native.apple.f9d69f27f9d7f4de" - }, - { - "kind": "ui-call", - "line": 92, - "path": "apps/ios/Sources/Voice/TalkPermissionPromptView.swift", - "source": "Retry", - "surface": "apple", - "id": "native.apple.0c5336129f70faed" - }, - { - "kind": "conditional-branch", - "line": 138, - "path": "apps/ios/Sources/Voice/TalkPermissionPromptView.swift", - "source": "Sending approval request", - "surface": "apple", - "id": "native.apple.7b23f0a89d3ad411" - }, - { - "kind": "conditional-branch", - "line": 140, - "path": "apps/ios/Sources/Voice/TalkPermissionPromptView.swift", - "source": "Approval sent", - "surface": "apple", - "id": "native.apple.55db7317a2542e51" - }, - { - "kind": "conditional-branch", - "line": 142, - "path": "apps/ios/Sources/Voice/TalkPermissionPromptView.swift", - "source": "Could not request approval", - "surface": "apple", - "id": "native.apple.06330f62ab254a0e" - }, - { - "kind": "conditional-branch", - "line": 144, - "path": "apps/ios/Sources/Voice/TalkPermissionPromptView.swift", - "source": "Enable Talk", - "surface": "apple", - "id": "native.apple.6580d3d1cdda682d" - }, - { - "kind": "conditional-branch", - "line": 151, - "path": "apps/ios/Sources/Voice/TalkPermissionPromptView.swift", - "source": "Sending a new pairing request to your gateway...", - "surface": "apple", - "id": "native.apple.abbfc1ba85a230b4" - }, - { - "kind": "conditional-branch", - "line": 153, - "path": "apps/ios/Sources/Voice/TalkPermissionPromptView.swift", - "source": "Approve this request on your gateway. Talk will start automatically when approval lands.", - "surface": "apple", - "id": "native.apple.2880deb53ea2fd86" - }, - { - "kind": "conditional-branch", - "line": 155, - "path": "apps/ios/Sources/Voice/TalkPermissionPromptView.swift", - "source": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider.", - "surface": "apple", - "id": "native.apple.80faa829f543c03c" - }, - { - "kind": "conditional-branch", - "line": 161, - "path": "apps/ios/Sources/Voice/TalkPermissionPromptView.swift", - "source": "Request Again", - "surface": "apple", - "id": "native.apple.88b0a2e2986fcebf" - }, - { - "kind": "conditional-branch", - "line": 161, - "path": "apps/ios/Sources/Voice/TalkPermissionPromptView.swift", - "source": "Send Approval Request", - "surface": "apple", - "id": "native.apple.5a3faae3472b4b86" - }, { "kind": "conditional-branch", "line": 446, @@ -36499,7 +36323,7 @@ }, { "kind": "ui-localized-call", - "line": 251, + "line": 415, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift", "source": "Context %@%% used", "surface": "apple", @@ -36507,7 +36331,7 @@ }, { "kind": "ui-localized-call", - "line": 265, + "line": 429, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift", "source": "%@ (override)", "surface": "apple", @@ -36515,7 +36339,7 @@ }, { "kind": "ui-call", - "line": 271, + "line": 435, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift", "source": "Thinking", "surface": "apple", @@ -36523,7 +36347,7 @@ }, { "kind": "ui-localized-call", - "line": 291, + "line": 455, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift", "source": "Full", "surface": "apple", @@ -36531,7 +36355,7 @@ }, { "kind": "ui-localized-call", - "line": 298, + "line": 462, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift", "source": "Verbosity", "surface": "apple", @@ -36539,7 +36363,7 @@ }, { "kind": "ui-localized-call", - "line": 307, + "line": 471, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift", "source": "Default (inherited)", "surface": "apple", @@ -36547,7 +36371,7 @@ }, { "kind": "ui-localized-call", - "line": 309, + "line": 473, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift", "source": "On", "surface": "apple", @@ -36555,7 +36379,7 @@ }, { "kind": "ui-localized-call", - "line": 310, + "line": 474, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift", "source": "Off", "surface": "apple", @@ -36563,7 +36387,7 @@ }, { "kind": "ui-localized-call", - "line": 312, + "line": 476, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift", "source": "Fast", "surface": "apple", @@ -36571,7 +36395,7 @@ }, { "kind": "ui-localized-call", - "line": 317, + "line": 481, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift", "source": "Fast responses", "surface": "apple", @@ -36579,7 +36403,7 @@ }, { "kind": "ui-call", - "line": 336, + "line": 500, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift", "source": "Pinned", "surface": "apple", @@ -36587,7 +36411,7 @@ }, { "kind": "ui-call", - "line": 344, + "line": 508, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift", "source": "Recent", "surface": "apple", @@ -36595,7 +36419,7 @@ }, { "kind": "ui-call", - "line": 363, + "line": 527, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift", "source": "Model", "surface": "apple", @@ -36603,7 +36427,7 @@ }, { "kind": "ui-localized-call", - "line": 380, + "line": 544, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift", "source": "Default", "surface": "apple", @@ -36611,7 +36435,7 @@ }, { "kind": "conditional-branch", - "line": 397, + "line": 561, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift", "source": "Pin model", "surface": "apple", @@ -36619,7 +36443,7 @@ }, { "kind": "conditional-branch", - "line": 397, + "line": 561, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift", "source": "Unpin model", "surface": "apple", @@ -36627,7 +36451,7 @@ }, { "kind": "ui-call", - "line": 411, + "line": 575, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift", "source": "Session", "surface": "apple", @@ -36635,7 +36459,7 @@ }, { "kind": "ui-modifier", - "line": 466, + "line": 631, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift", "source": "Add Image", "surface": "apple", @@ -36643,23 +36467,15 @@ }, { "kind": "ui-modifier", - "line": 467, + "line": 632, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift", "source": "Attachments", "surface": "apple", "id": "native.apple.1963dfa3285cb715" }, - { - "kind": "ui-call", - "line": 494, - "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift", - "source": "Voice note", - "surface": "apple", - "id": "native.apple.f8dda832ed76441d" - }, { "kind": "conditional-branch", - "line": 647, + "line": 810, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift", "source": "Talk", "surface": "apple", @@ -36667,7 +36483,7 @@ }, { "kind": "conditional-branch", - "line": 694, + "line": 881, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift", "source": "Start realtime chat", "surface": "apple", @@ -36675,7 +36491,7 @@ }, { "kind": "conditional-branch", - "line": 694, + "line": 881, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift", "source": "Stop realtime chat", "surface": "apple", @@ -36683,7 +36499,7 @@ }, { "kind": "conditional-branch", - "line": 728, + "line": 915, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift", "source": "Start", "surface": "apple", @@ -36691,7 +36507,7 @@ }, { "kind": "conditional-branch", - "line": 728, + "line": 915, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift", "source": "Stop", "surface": "apple", @@ -36699,7 +36515,7 @@ }, { "kind": "ui-call", - "line": 845, + "line": 1032, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift", "source": "Loading commands", "surface": "apple", @@ -36707,7 +36523,7 @@ }, { "kind": "ui-call", - "line": 854, + "line": 1041, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift", "source": "Commands unavailable", "surface": "apple", @@ -36715,7 +36531,7 @@ }, { "kind": "ui-call", - "line": 863, + "line": 1050, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift", "source": "Retry", "surface": "apple", @@ -36723,7 +36539,7 @@ }, { "kind": "ui-call", - "line": 872, + "line": 1059, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift", "source": "No matching commands", "surface": "apple", @@ -36731,7 +36547,7 @@ }, { "kind": "ui-modifier", - "line": 1104, + "line": 1291, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift", "source": "Stop response", "surface": "apple", @@ -36739,7 +36555,7 @@ }, { "kind": "ui-modifier", - "line": 1129, + "line": 1316, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift", "source": "Send message", "surface": "apple", @@ -36747,7 +36563,7 @@ }, { "kind": "ui-modifier", - "line": 1143, + "line": 1330, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift", "source": "Refresh", "surface": "apple", @@ -36755,7 +36571,7 @@ }, { "kind": "conditional-branch", - "line": 1277, + "line": 1455, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift", "source": "Connecting...", "surface": "apple", @@ -36763,7 +36579,7 @@ }, { "kind": "conditional-branch", - "line": 1277, + "line": 1455, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift", "source": "Gateway connected", "surface": "apple", @@ -36771,7 +36587,7 @@ }, { "kind": "conditional-branch", - "line": 1286, + "line": 1464, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift", "source": "Message…", "surface": "apple", @@ -38131,7 +37947,7 @@ }, { "kind": "ui-call", - "line": 487, + "line": 491, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift", "source": "Retry Send", "surface": "apple", @@ -38139,7 +37955,7 @@ }, { "kind": "ui-call", - "line": 501, + "line": 505, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift", "source": "Delete", "surface": "apple", @@ -38147,7 +37963,7 @@ }, { "kind": "ui-call", - "line": 535, + "line": 539, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift", "source": "Stop Listening", "surface": "apple", @@ -38155,7 +37971,7 @@ }, { "kind": "ui-call", - "line": 538, + "line": 542, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift", "source": "Listen", "surface": "apple", @@ -38163,7 +37979,7 @@ }, { "kind": "ui-modifier", - "line": 629, + "line": 633, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift", "source": "Jump to latest reply", "surface": "apple", @@ -38171,7 +37987,7 @@ }, { "kind": "ui-named-argument", - "line": 649, + "line": 653, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift", "source": "Refresh", "surface": "apple", @@ -38179,7 +37995,7 @@ }, { "kind": "ui-call", - "line": 1036, + "line": 1040, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift", "source": "Copy Message", "surface": "apple", @@ -38187,7 +38003,7 @@ }, { "kind": "ui-call", - "line": 1059, + "line": 1063, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift", "source": "Open Full Message", "surface": "apple", @@ -38195,7 +38011,7 @@ }, { "kind": "ui-localized-call", - "line": 1080, + "line": 1084, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift", "source": "Reply", "surface": "apple", @@ -38203,7 +38019,7 @@ }, { "kind": "ui-localized-call", - "line": 1090, + "line": 1094, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift", "source": "You", "surface": "apple", @@ -38211,7 +38027,7 @@ }, { "kind": "ui-localized-call", - "line": 1092, + "line": 1096, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift", "source": "Assistant", "surface": "apple", @@ -38219,7 +38035,7 @@ }, { "kind": "ui-call", - "line": 1158, + "line": 1162, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift", "source": "Loading chat", "surface": "apple", @@ -38227,7 +38043,7 @@ }, { "kind": "ui-modifier", - "line": 1238, + "line": 1242, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift", "source": "Dismiss", "surface": "apple", @@ -38235,7 +38051,15 @@ }, { "kind": "ui-localized-call", - "line": 27, + "line": 74, + "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel+Attachments.swift", + "source": "Remove attachments or wait for delivery to resolve before starting a new chat.", + "surface": "apple", + "id": "native.apple.3892b7f55d9d1f0a" + }, + { + "kind": "ui-localized-call", + "line": 112, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel+Attachments.swift", "source": "Could not attach voice note: %@", "surface": "apple", @@ -38243,7 +38067,7 @@ }, { "kind": "ui-localized-call", - "line": 33, + "line": 118, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel+Attachments.swift", "source": "Voice note exceeds the 5 MB attachment limit", "surface": "apple", @@ -38251,7 +38075,7 @@ }, { "kind": "ui-localized-call", - "line": 79, + "line": 180, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel+Attachments.swift", "source": "Only image attachments are supported right now", "surface": "apple", @@ -38259,7 +38083,7 @@ }, { "kind": "ui-localized-call", - "line": 90, + "line": 192, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel+Attachments.swift", "source": "Could not process %1$@: %2$@", "surface": "apple", @@ -38267,7 +38091,7 @@ }, { "kind": "ui-localized-call", - "line": 98, + "line": 203, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel+Attachments.swift", "source": "Attachment %@ exceeds 5 MB limit after resizing", "surface": "apple", @@ -38275,7 +38099,7 @@ }, { "kind": "conditional-branch", - "line": 105, + "line": 210, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel+Attachments.swift", "source": "\\(baseName).jpg", "surface": "apple", @@ -38283,7 +38107,7 @@ }, { "kind": "conditional-branch", - "line": 43, + "line": 66, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel+Sending.swift", "source": "\\(invocation) ", "surface": "apple", @@ -38291,7 +38115,7 @@ }, { "kind": "conditional-branch", - "line": 295, + "line": 324, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel+Sending.swift", "source": "See attached.", "surface": "apple", @@ -38299,7 +38123,7 @@ }, { "kind": "conditional-branch", - "line": 700, + "line": 735, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel+Sending.swift", "source": "delivery unconfirmed", "surface": "apple", @@ -38307,7 +38131,7 @@ }, { "kind": "conditional-branch", - "line": 700, + "line": 735, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel+Sending.swift", "source": "queued after route change", "surface": "apple", @@ -38331,15 +38155,7 @@ }, { "kind": "ui-localized-call", - "line": 773, - "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift", - "source": "Remove attachments or wait for delivery to resolve before starting a new chat.", - "surface": "apple", - "id": "native.apple.d93e6fe16ec1b0e5" - }, - { - "kind": "ui-localized-call", - "line": 1156, + "line": 1082, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift", "source": "Remove attachments or wait for delivery to resolve before switching chats.", "surface": "apple", @@ -38697,6 +38513,94 @@ "surface": "apple", "id": "native.apple.f0a4720df5f5f403" }, + { + "kind": "ui-call", + "line": 70, + "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/CleanChatComposerControls.swift", + "source": "Voice note", + "surface": "apple", + "id": "native.apple.fa10740f9e272e83" + }, + { + "kind": "ui-call", + "line": 120, + "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/CleanChatComposerControls.swift", + "source": "Photo Library", + "surface": "apple", + "id": "native.apple.e4de5afc37981d6c" + }, + { + "kind": "ui-call", + "line": 132, + "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/CleanChatComposerControls.swift", + "source": "Camera", + "surface": "apple", + "id": "native.apple.37c19a66626b21fa" + }, + { + "kind": "ui-call", + "line": 145, + "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/CleanChatComposerControls.swift", + "source": "Choose Image File", + "surface": "apple", + "id": "native.apple.333682684163ba6b" + }, + { + "kind": "ui-modifier", + "line": 156, + "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/CleanChatComposerControls.swift", + "source": "Add attachment", + "surface": "apple", + "id": "native.apple.534246792690968d" + }, + { + "kind": "ui-call", + "line": 282, + "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/CleanChatComposerControls.swift", + "source": "Record Voice Note", + "surface": "apple", + "id": "native.apple.e33210feaa3da016" + }, + { + "kind": "conditional-branch", + "line": 332, + "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/CleanChatComposerControls.swift", + "source": "Dictate message", + "surface": "apple", + "id": "native.apple.00f0e15858e38d19" + }, + { + "kind": "conditional-branch", + "line": 333, + "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/CleanChatComposerControls.swift", + "source": "Listening", + "surface": "apple", + "id": "native.apple.96f20175f61a2b29" + }, + { + "kind": "conditional-branch", + "line": 333, + "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/CleanChatComposerControls.swift", + "source": "Not listening", + "surface": "apple", + "id": "native.apple.eaa9047db6cfb67e" + }, + { + "kind": "conditional-branch", + "line": 335, + "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/CleanChatComposerControls.swift", + "source": "Finish dictation", + "surface": "apple", + "id": "native.apple.4185df7a940f09ef" + }, + { + "kind": "conditional-branch", + "line": 335, + "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/CleanChatComposerControls.swift", + "source": "Transcribe speech into the message", + "surface": "apple", + "id": "native.apple.5a0793d2827b9399" + }, { "kind": "ui-call", "line": 592, @@ -38723,7 +38627,7 @@ }, { "kind": "ui-modifier", - "line": 63, + "line": 64, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/VoiceNoteComposerViews.swift", "source": "Recording", "surface": "apple", @@ -38731,7 +38635,7 @@ }, { "kind": "ui-modifier", - "line": 78, + "line": 79, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/VoiceNoteComposerViews.swift", "source": "Cancel voice note", "surface": "apple", @@ -38739,7 +38643,7 @@ }, { "kind": "ui-modifier", - "line": 87, + "line": 88, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/VoiceNoteComposerViews.swift", "source": "Finish voice note", "surface": "apple", diff --git a/apps/.i18n/native/ar.json b/apps/.i18n/native/ar.json index 6844967207c6..ec4a20aff4aa 100644 --- a/apps/.i18n/native/ar.json +++ b/apps/.i18n/native/ar.json @@ -8358,11 +8358,21 @@ "source": "OpenClaw Activity", "translated": "نشاط OpenClaw" }, + { + "id": "native.apple.093081590556db7f", + "source": "OPENCLAW", + "translated": "OPENCLAW" + }, { "id": "native.apple.86d546e1f1247028", "source": "OpenClaw", "translated": "OpenClaw" }, + { + "id": "native.apple.9fd01bdb5a753f3e", + "source": "LIVE", + "translated": "LIVE" + }, { "id": "native.apple.417ac9273b5f63ca", "source": "Connecting...", @@ -8383,6 +8393,36 @@ "source": "Action required", "translated": "إجراء مطلوب" }, + { + "id": "native.apple.275026f681242715", + "source": "Using %@", + "translated": "استخدام %@" + }, + { + "id": "native.apple.360d0d0fc0df9eb2", + "source": "Using a tool", + "translated": "استخدام أداة" + }, + { + "id": "native.apple.0d0ee660962ea840", + "source": "Listening", + "translated": "جارٍ الاستماع" + }, + { + "id": "native.apple.095ebf1496feb2ad", + "source": "Speaking", + "translated": "جارٍ النطق" + }, + { + "id": "native.apple.63c98d2ca6a5b09c", + "source": "Voice active", + "translated": "الصوت مفعّل" + }, + { + "id": "native.apple.02470f4b5348f97c", + "source": "Paused", + "translated": "متوقفة مؤقتًا" + }, { "id": "native.apple.f2f3e6437fafcd1c", "source": "Connected", @@ -10518,11 +10558,36 @@ "source": "The session attaches once the gateway is ready.", "translated": "يتم إرفاق الجلسة بمجرد أن يصبح Gateway جاهزًا." }, + { + "id": "native.apple.02611ddddef7295a", + "source": "Collapsed", + "translated": "مطوي" + }, + { + "id": "native.apple.1f4911f4a05230b5", + "source": "Expanded", + "translated": "موسّع" + }, { "id": "native.apple.d19c9577e0fdfea7", "source": "Opens Settings / Gateway", "translated": "يفتح Settings / Gateway" }, + { + "id": "native.apple.b59e690104ae3e24", + "source": "Hides the gateway status label", + "translated": "يخفي تسمية حالة البوابة" + }, + { + "id": "native.apple.5fcf9d462463dd27", + "source": "Shows the full gateway status label", + "translated": "يعرض تسمية حالة البوابة كاملة" + }, + { + "id": "native.apple.592b1e673e42df50", + "source": "Voice off", + "translated": "الصوت متوقف" + }, { "id": "native.apple.7c87384c7d1370e1", "source": "New Chat", @@ -10548,6 +10613,11 @@ "source": "Export Transcript", "translated": "تصدير النص" }, + { + "id": "native.apple.d510e012c91b24ed", + "source": "Gateway Settings", + "translated": "إعدادات البوابة" + }, { "id": "native.apple.f26cef22f417abb4", "source": "Show reasoning & tool activity", @@ -11728,11 +11798,6 @@ "source": "Agent chat and recent work.", "translated": "محادثة الوكيل والعمل الأخير." }, - { - "id": "native.apple.a30d0a950d0a77aa", - "source": "Realtime voice and controls.", - "translated": "الصوت والتحكم في الوقت الفعلي." - }, { "id": "native.apple.a03e961ab14ba060", "source": "Overview", @@ -13818,151 +13883,6 @@ "source": "ClawHub did not report an installable version for this skill.", "translated": "لم يُبلغ ClawHub عن إصدار قابل للتثبيت لهذه المهارة." }, - { - "id": "native.apple.798c3584ad95f8da", - "source": "Enable Talk", - "translated": "تمكين التحدث" - }, - { - "id": "native.apple.c6e171128190d131", - "source": "Not Now", - "translated": "ليس الآن" - }, - { - "id": "native.apple.e4b15aef3c2e3da7", - "source": "Talk", - "translated": "تحدث" - }, - { - "id": "native.apple.ab3381742bdff650", - "source": "Conversation", - "translated": "المحادثة" - }, - { - "id": "native.apple.7af100f6640c6a69", - "source": "Agent", - "translated": "الوكيل" - }, - { - "id": "native.apple.49c4de60117b1a98", - "source": "Session", - "translated": "الجلسة" - }, - { - "id": "native.apple.37c29d2506b955e9", - "source": "Runtime", - "translated": "وقت التشغيل" - }, - { - "id": "native.apple.13e0ad79b141576e", - "source": "Voice Mode", - "translated": "وضع الصوت" - }, - { - "id": "native.apple.e8035b71bab833c7", - "source": "Configured", - "translated": "مهيأ" - }, - { - "id": "native.apple.1b2845ec89d20eef", - "source": "Active", - "translated": "نشط" - }, - { - "id": "native.apple.a82c07df64d13b23", - "source": "Transport", - "translated": "النقل" - }, - { - "id": "native.apple.384e097278f76954", - "source": "Last issue", - "translated": "آخر مشكلة" - }, - { - "id": "native.apple.a6d8fc7bfd98836f", - "source": "Permission", - "translated": "الإذن" - }, - { - "id": "native.apple.983bdcd5e3f1baa8", - "source": "Speech language", - "translated": "لغة الكلام" - }, - { - "id": "native.apple.32e7fa2b1798e9c3", - "source": "Controls", - "translated": "عناصر التحكم" - }, - { - "id": "native.apple.0eb012a5d3d81b32", - "source": "Speakerphone", - "translated": "مكبر الصوت" - }, - { - "id": "native.apple.ef8699e2718346da", - "source": "Background listening", - "translated": "الاستماع في الخلفية" - }, - { - "id": "native.apple.1e6938a09b4fbe77", - "source": "Voice & Talk Settings", - "translated": "إعدادات الصوت والتحدث" - }, - { - "id": "native.apple.c7a591fd5bb6687e", - "source": "Gateway approval is required before this phone can capture voice.", - "translated": "يلزم الحصول على موافقة Gateway قبل أن يتمكن هذا الهاتف من التقاط الصوت." - }, - { - "id": "native.apple.9d40e75e55ed39eb", - "source": "Voice is disabled in Apple Review demo mode.", - "translated": "الصوت معطّل في وضع العرض التوضيحي لمراجعة Apple." - }, - { - "id": "native.apple.3ef69bf916177799", - "source": "Connect to your gateway to start a voice conversation.", - "translated": "اتصل بـ Gateway لبدء محادثة صوتية." - }, - { - "id": "native.apple.ed03af7c68841a5d", - "source": "Open Voice settings after the gateway loads Talk configuration.", - "translated": "افتح إعدادات الصوت بعد أن يحمّل Gateway إعدادات Talk." - }, - { - "id": "native.apple.883a251978601b96", - "source": "Routes voice to %@.", - "translated": "يوجّه الصوت إلى %@." - }, - { - "id": "native.apple.877fb5c1abfaafa1", - "source": "Not loaded", - "translated": "Not loaded" - }, - { - "id": "native.apple.d3ccb092936551fc", - "source": "Not active", - "translated": "غير نشط" - }, - { - "id": "native.apple.1620f7cb3deea133", - "source": "Open Gateway Settings", - "translated": "فتح إعدادات Gateway" - }, - { - "id": "native.apple.3fd2706ef0f14fd2", - "source": "Open Voice Settings", - "translated": "فتح إعدادات الصوت" - }, - { - "id": "native.apple.3cc9cd23a67db077", - "source": "Demo Mode Only", - "translated": "وضع العرض التوضيحي فقط" - }, - { - "id": "native.apple.12c9d8231777366c", - "source": "Waiting for Approval", - "translated": "في انتظار الموافقة" - }, { "id": "native.apple.dc57861a3ac5a08e", "source": "Open Settings", @@ -14528,6 +14448,41 @@ "source": "Action required", "translated": "إجراء مطلوب" }, + { + "id": "native.apple.7205f0822706cb55", + "source": "Listening", + "translated": "جارٍ الاستماع" + }, + { + "id": "native.apple.8b10cb1dc21e42af", + "source": "Listening (PTT)", + "translated": "جارٍ الاستماع (PTT)" + }, + { + "id": "native.apple.939c7ec55636c67f", + "source": "Listening (Realtime)", + "translated": "جارٍ الاستماع (في الوقت الفعلي)" + }, + { + "id": "native.apple.86f203ebb2453d15", + "source": "Speaking…", + "translated": "يتحدث…" + }, + { + "id": "native.apple.1e3ff6b56ae3828d", + "source": "Speaking (System)…", + "translated": "جارٍ النطق (النظام)…" + }, + { + "id": "native.apple.37a5fedaefb8ebf3", + "source": "Generating voice…", + "translated": "جارٍ إنشاء الصوت…" + }, + { + "id": "native.apple.47003f094346c660", + "source": "Ready", + "translated": "جاهز" + }, { "id": "native.apple.cf0450b542893fb5", "source": "Off", @@ -15348,26 +15303,6 @@ "source": "unknown relay error", "translated": "unknown relay error" }, - { - "id": "native.apple.f2ea327bfea8b8e3", - "source": "Talk", - "translated": "تحدّث" - }, - { - "id": "native.apple.bda6cfa988e49a75", - "source": "Control", - "translated": "التحكم" - }, - { - "id": "native.apple.3c80f5351055a5f3", - "source": "Agent", - "translated": "الوكيل" - }, - { - "id": "native.apple.22d107db5b905ef4", - "source": "Settings", - "translated": "الإعدادات" - }, { "id": "native.apple.c23d35de8d70863f", "source": "OpenClaw", @@ -15458,6 +15393,26 @@ "source": "Close canvas", "translated": "إغلاق اللوحة" }, + { + "id": "native.apple.a1db6ce0aed1d18e", + "source": "Chat, voice active", + "translated": "دردشة، الصوت مفعّل" + }, + { + "id": "native.apple.bda6cfa988e49a75", + "source": "Control", + "translated": "التحكم" + }, + { + "id": "native.apple.3c80f5351055a5f3", + "source": "Agent", + "translated": "الوكيل" + }, + { + "id": "native.apple.22d107db5b905ef4", + "source": "Settings", + "translated": "الإعدادات" + }, { "id": "native.apple.8a4751c86aa6fe77", "source": "Gateway needs attention", @@ -15488,11 +15443,6 @@ "source": "Chat", "translated": "الدردشة" }, - { - "id": "native.apple.febca7528af94342", - "source": "Talk", - "translated": "التحدث" - }, { "id": "native.apple.e26bbe303d8a9544", "source": "Overview", @@ -15863,6 +15813,11 @@ "source": "Requesting permissions…", "translated": "جارٍ طلب الأذونات…" }, + { + "id": "native.apple.198f59c97d7afe11", + "source": "Start failed: %@", + "translated": "فشل البدء: %@" + }, { "id": "native.apple.a1910b5c2e008b6c", "source": "Microphone permission denied", @@ -15873,11 +15828,6 @@ "source": "Speech recognition", "translated": "التعرّف على الكلام" }, - { - "id": "native.apple.198f59c97d7afe11", - "source": "Start failed: %@", - "translated": "فشل البدء: %@" - }, { "id": "native.apple.c09f4e3bbef8177a", "source": "Offline", @@ -16088,66 +16038,6 @@ "source": "Listening (PTT)", "translated": "جارٍ الاستماع (PTT)" }, - { - "id": "native.apple.0189269b6a863dc2", - "source": "Request ID", - "translated": "معرّف الطلب" - }, - { - "id": "native.apple.f9d69f27f9d7f4de", - "source": "Sending...", - "translated": "جارٍ الإرسال..." - }, - { - "id": "native.apple.0c5336129f70faed", - "source": "Retry", - "translated": "إعادة المحاولة" - }, - { - "id": "native.apple.7b23f0a89d3ad411", - "source": "Sending approval request", - "translated": "جارٍ إرسال طلب الموافقة" - }, - { - "id": "native.apple.55db7317a2542e51", - "source": "Approval sent", - "translated": "تم إرسال الموافقة" - }, - { - "id": "native.apple.06330f62ab254a0e", - "source": "Could not request approval", - "translated": "تعذّر طلب الموافقة" - }, - { - "id": "native.apple.6580d3d1cdda682d", - "source": "Enable Talk", - "translated": "تفعيل Talk" - }, - { - "id": "native.apple.abbfc1ba85a230b4", - "source": "Sending a new pairing request to your gateway...", - "translated": "جارٍ إرسال طلب اقتران جديد إلى gateway الخاص بك..." - }, - { - "id": "native.apple.2880deb53ea2fd86", - "source": "Approve this request on your gateway. Talk will start automatically when approval lands.", - "translated": "وافق على هذا الطلب على gateway الخاص بك. سيبدأ Talk تلقائيًا عند وصول الموافقة." - }, - { - "id": "native.apple.80faa829f543c03c", - "source": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider.", - "translated": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider." - }, - { - "id": "native.apple.88b0a2e2986fcebf", - "source": "Request Again", - "translated": "الطلب مرة أخرى" - }, - { - "id": "native.apple.5a3faae3472b4b86", - "source": "Send Approval Request", - "translated": "إرسال طلب الموافقة" - }, { "id": "native.apple.1065cfe92d7c1eb6", "source": "Asking OpenClaw", @@ -22908,11 +22798,6 @@ "source": "Attachments", "translated": "المرفقات" }, - { - "id": "native.apple.f8dda832ed76441d", - "source": "Voice note", - "translated": "ملاحظة صوتية" - }, { "id": "native.apple.ee2c0d22f5861159", "source": "Talk", @@ -23898,6 +23783,11 @@ "source": "Dismiss", "translated": "إغلاق" }, + { + "id": "native.apple.3892b7f55d9d1f0a", + "source": "Remove attachments or wait for delivery to resolve before starting a new chat.", + "translated": "أزِل المرفقات أو انتظر اكتمال التسليم قبل بدء محادثة جديدة." + }, { "id": "native.apple.92fe9093903eda3b", "source": "Could not attach voice note: %@", @@ -23958,11 +23848,6 @@ "source": "Gateway changed before the session operation started.", "translated": "تغيّر Gateway قبل بدء عملية الجلسة." }, - { - "id": "native.apple.d93e6fe16ec1b0e5", - "source": "Remove attachments or wait for delivery to resolve before starting a new chat.", - "translated": "أزِل المرفقات أو انتظر اكتمال التسليم قبل بدء محادثة جديدة." - }, { "id": "native.apple.710c7bd117a7cc12", "source": "Remove attachments or wait for delivery to resolve before switching chats.", @@ -24188,6 +24073,61 @@ "source": "Compact Session", "translated": "ضغط الجلسة" }, + { + "id": "native.apple.fa10740f9e272e83", + "source": "Voice note", + "translated": "ملاحظة صوتية" + }, + { + "id": "native.apple.e4de5afc37981d6c", + "source": "Photo Library", + "translated": "مكتبة الصور" + }, + { + "id": "native.apple.37c19a66626b21fa", + "source": "Camera", + "translated": "الكاميرا" + }, + { + "id": "native.apple.333682684163ba6b", + "source": "Choose Image File", + "translated": "اختر ملف صورة" + }, + { + "id": "native.apple.534246792690968d", + "source": "Add attachment", + "translated": "إضافة مرفق" + }, + { + "id": "native.apple.e33210feaa3da016", + "source": "Record Voice Note", + "translated": "تسجيل ملاحظة صوتية" + }, + { + "id": "native.apple.00f0e15858e38d19", + "source": "Dictate message", + "translated": "إملاء الرسالة" + }, + { + "id": "native.apple.96f20175f61a2b29", + "source": "Listening", + "translated": "جارٍ الاستماع" + }, + { + "id": "native.apple.eaa9047db6cfb67e", + "source": "Not listening", + "translated": "لا يستمع" + }, + { + "id": "native.apple.4185df7a940f09ef", + "source": "Finish dictation", + "translated": "إنهاء الإملاء" + }, + { + "id": "native.apple.5a0793d2827b9399", + "source": "Transcribe speech into the message", + "translated": "حوّل الكلام إلى نص داخل الرسالة." + }, { "id": "native.apple.f74d150ce05205dd", "source": "z", diff --git a/apps/.i18n/native/de.json b/apps/.i18n/native/de.json index bbfb8ff7f3b9..854077b1e691 100644 --- a/apps/.i18n/native/de.json +++ b/apps/.i18n/native/de.json @@ -8358,11 +8358,21 @@ "source": "OpenClaw Activity", "translated": "OpenClaw-Aktivität" }, + { + "id": "native.apple.093081590556db7f", + "source": "OPENCLAW", + "translated": "OPENCLAW" + }, { "id": "native.apple.86d546e1f1247028", "source": "OpenClaw", "translated": "OpenClaw" }, + { + "id": "native.apple.9fd01bdb5a753f3e", + "source": "LIVE", + "translated": "LIVE" + }, { "id": "native.apple.417ac9273b5f63ca", "source": "Connecting...", @@ -8383,6 +8393,36 @@ "source": "Action required", "translated": "Aktion erforderlich" }, + { + "id": "native.apple.275026f681242715", + "source": "Using %@", + "translated": "Mit %@" + }, + { + "id": "native.apple.360d0d0fc0df9eb2", + "source": "Using a tool", + "translated": "Tool verwenden" + }, + { + "id": "native.apple.0d0ee660962ea840", + "source": "Listening", + "translated": "Hört zu" + }, + { + "id": "native.apple.095ebf1496feb2ad", + "source": "Speaking", + "translated": "Spricht" + }, + { + "id": "native.apple.63c98d2ca6a5b09c", + "source": "Voice active", + "translated": "Sprache aktiv" + }, + { + "id": "native.apple.02470f4b5348f97c", + "source": "Paused", + "translated": "Pausiert" + }, { "id": "native.apple.f2f3e6437fafcd1c", "source": "Connected", @@ -10518,11 +10558,36 @@ "source": "The session attaches once the gateway is ready.", "translated": "Die Sitzung wird angehängt, sobald die Gateway bereit ist." }, + { + "id": "native.apple.02611ddddef7295a", + "source": "Collapsed", + "translated": "Eingeklappt" + }, + { + "id": "native.apple.1f4911f4a05230b5", + "source": "Expanded", + "translated": "Ausgeklappt" + }, { "id": "native.apple.d19c9577e0fdfea7", "source": "Opens Settings / Gateway", "translated": "Öffnet Einstellungen / Gateway" }, + { + "id": "native.apple.b59e690104ae3e24", + "source": "Hides the gateway status label", + "translated": "Blendet die Gateway-Statusbezeichnung aus" + }, + { + "id": "native.apple.5fcf9d462463dd27", + "source": "Shows the full gateway status label", + "translated": "Zeigt die vollständige Gateway-Statusbezeichnung an" + }, + { + "id": "native.apple.592b1e673e42df50", + "source": "Voice off", + "translated": "Sprache aus" + }, { "id": "native.apple.7c87384c7d1370e1", "source": "New Chat", @@ -10548,6 +10613,11 @@ "source": "Export Transcript", "translated": "Transkript exportieren" }, + { + "id": "native.apple.d510e012c91b24ed", + "source": "Gateway Settings", + "translated": "Gateway-Einstellungen" + }, { "id": "native.apple.f26cef22f417abb4", "source": "Show reasoning & tool activity", @@ -11728,11 +11798,6 @@ "source": "Agent chat and recent work.", "translated": "Agent-Chat und aktuelle Arbeit." }, - { - "id": "native.apple.a30d0a950d0a77aa", - "source": "Realtime voice and controls.", - "translated": "Echtzeit-Sprache und -Steuerung." - }, { "id": "native.apple.a03e961ab14ba060", "source": "Overview", @@ -13818,151 +13883,6 @@ "source": "ClawHub did not report an installable version for this skill.", "translated": "ClawHub hat keine installierbare Version für diesen Skill gemeldet." }, - { - "id": "native.apple.798c3584ad95f8da", - "source": "Enable Talk", - "translated": "Talk aktivieren" - }, - { - "id": "native.apple.c6e171128190d131", - "source": "Not Now", - "translated": "Nicht jetzt" - }, - { - "id": "native.apple.e4b15aef3c2e3da7", - "source": "Talk", - "translated": "Sprechen" - }, - { - "id": "native.apple.ab3381742bdff650", - "source": "Conversation", - "translated": "Unterhaltung" - }, - { - "id": "native.apple.7af100f6640c6a69", - "source": "Agent", - "translated": "Agent" - }, - { - "id": "native.apple.49c4de60117b1a98", - "source": "Session", - "translated": "Sitzung" - }, - { - "id": "native.apple.37c29d2506b955e9", - "source": "Runtime", - "translated": "Laufzeit" - }, - { - "id": "native.apple.13e0ad79b141576e", - "source": "Voice Mode", - "translated": "Sprachmodus" - }, - { - "id": "native.apple.e8035b71bab833c7", - "source": "Configured", - "translated": "Konfiguriert" - }, - { - "id": "native.apple.1b2845ec89d20eef", - "source": "Active", - "translated": "Aktiv" - }, - { - "id": "native.apple.a82c07df64d13b23", - "source": "Transport", - "translated": "Transport" - }, - { - "id": "native.apple.384e097278f76954", - "source": "Last issue", - "translated": "Letztes Problem" - }, - { - "id": "native.apple.a6d8fc7bfd98836f", - "source": "Permission", - "translated": "Berechtigung" - }, - { - "id": "native.apple.983bdcd5e3f1baa8", - "source": "Speech language", - "translated": "Sprache für Spracherkennung" - }, - { - "id": "native.apple.32e7fa2b1798e9c3", - "source": "Controls", - "translated": "Steuerelemente" - }, - { - "id": "native.apple.0eb012a5d3d81b32", - "source": "Speakerphone", - "translated": "Freisprechen" - }, - { - "id": "native.apple.ef8699e2718346da", - "source": "Background listening", - "translated": "Mithören im Hintergrund" - }, - { - "id": "native.apple.1e6938a09b4fbe77", - "source": "Voice & Talk Settings", - "translated": "Einstellungen für Stimme & Sprechen" - }, - { - "id": "native.apple.c7a591fd5bb6687e", - "source": "Gateway approval is required before this phone can capture voice.", - "translated": "Die Genehmigung durch das Gateway ist erforderlich, bevor dieses Telefon Sprache erfassen kann." - }, - { - "id": "native.apple.9d40e75e55ed39eb", - "source": "Voice is disabled in Apple Review demo mode.", - "translated": "Sprache ist im Apple Review-Demomodus deaktiviert." - }, - { - "id": "native.apple.3ef69bf916177799", - "source": "Connect to your gateway to start a voice conversation.", - "translated": "Verbinde dich mit deinem Gateway, um ein Sprachgespräch zu beginnen." - }, - { - "id": "native.apple.ed03af7c68841a5d", - "source": "Open Voice settings after the gateway loads Talk configuration.", - "translated": "Öffne die Spracheinstellungen, nachdem das Gateway die Talk-Konfiguration geladen hat." - }, - { - "id": "native.apple.883a251978601b96", - "source": "Routes voice to %@.", - "translated": "Leitet Sprache an %@ weiter." - }, - { - "id": "native.apple.877fb5c1abfaafa1", - "source": "Not loaded", - "translated": "Not loaded" - }, - { - "id": "native.apple.d3ccb092936551fc", - "source": "Not active", - "translated": "Nicht aktiv" - }, - { - "id": "native.apple.1620f7cb3deea133", - "source": "Open Gateway Settings", - "translated": "Gateway-Einstellungen öffnen" - }, - { - "id": "native.apple.3fd2706ef0f14fd2", - "source": "Open Voice Settings", - "translated": "Spracheinstellungen öffnen" - }, - { - "id": "native.apple.3cc9cd23a67db077", - "source": "Demo Mode Only", - "translated": "Nur Demomodus" - }, - { - "id": "native.apple.12c9d8231777366c", - "source": "Waiting for Approval", - "translated": "Warten auf Genehmigung" - }, { "id": "native.apple.dc57861a3ac5a08e", "source": "Open Settings", @@ -14528,6 +14448,41 @@ "source": "Action required", "translated": "Aktion erforderlich" }, + { + "id": "native.apple.7205f0822706cb55", + "source": "Listening", + "translated": "Hört zu" + }, + { + "id": "native.apple.8b10cb1dc21e42af", + "source": "Listening (PTT)", + "translated": "Hört zu (PTT)" + }, + { + "id": "native.apple.939c7ec55636c67f", + "source": "Listening (Realtime)", + "translated": "Hört zu (Echtzeit)" + }, + { + "id": "native.apple.86f203ebb2453d15", + "source": "Speaking…", + "translated": "Spricht…" + }, + { + "id": "native.apple.1e3ff6b56ae3828d", + "source": "Speaking (System)…", + "translated": "Spricht (System)…" + }, + { + "id": "native.apple.37a5fedaefb8ebf3", + "source": "Generating voice…", + "translated": "Stimme wird generiert…" + }, + { + "id": "native.apple.47003f094346c660", + "source": "Ready", + "translated": "Bereit" + }, { "id": "native.apple.cf0450b542893fb5", "source": "Off", @@ -15348,26 +15303,6 @@ "source": "unknown relay error", "translated": "unknown relay error" }, - { - "id": "native.apple.f2ea327bfea8b8e3", - "source": "Talk", - "translated": "Sprechen" - }, - { - "id": "native.apple.bda6cfa988e49a75", - "source": "Control", - "translated": "Steuerung" - }, - { - "id": "native.apple.3c80f5351055a5f3", - "source": "Agent", - "translated": "Agent" - }, - { - "id": "native.apple.22d107db5b905ef4", - "source": "Settings", - "translated": "Einstellungen" - }, { "id": "native.apple.c23d35de8d70863f", "source": "OpenClaw", @@ -15458,6 +15393,26 @@ "source": "Close canvas", "translated": "Canvas schließen" }, + { + "id": "native.apple.a1db6ce0aed1d18e", + "source": "Chat, voice active", + "translated": "Chat, Sprache aktiv" + }, + { + "id": "native.apple.bda6cfa988e49a75", + "source": "Control", + "translated": "Steuerung" + }, + { + "id": "native.apple.3c80f5351055a5f3", + "source": "Agent", + "translated": "Agent" + }, + { + "id": "native.apple.22d107db5b905ef4", + "source": "Settings", + "translated": "Einstellungen" + }, { "id": "native.apple.8a4751c86aa6fe77", "source": "Gateway needs attention", @@ -15488,11 +15443,6 @@ "source": "Chat", "translated": "Chat" }, - { - "id": "native.apple.febca7528af94342", - "source": "Talk", - "translated": "Sprechen" - }, { "id": "native.apple.e26bbe303d8a9544", "source": "Overview", @@ -15863,6 +15813,11 @@ "source": "Requesting permissions…", "translated": "Berechtigungen werden angefordert…" }, + { + "id": "native.apple.198f59c97d7afe11", + "source": "Start failed: %@", + "translated": "Start fehlgeschlagen: %@" + }, { "id": "native.apple.a1910b5c2e008b6c", "source": "Microphone permission denied", @@ -15873,11 +15828,6 @@ "source": "Speech recognition", "translated": "Spracherkennung" }, - { - "id": "native.apple.198f59c97d7afe11", - "source": "Start failed: %@", - "translated": "Start fehlgeschlagen: %@" - }, { "id": "native.apple.c09f4e3bbef8177a", "source": "Offline", @@ -16088,66 +16038,6 @@ "source": "Listening (PTT)", "translated": "Hört zu (PTT)" }, - { - "id": "native.apple.0189269b6a863dc2", - "source": "Request ID", - "translated": "Anfrage-ID" - }, - { - "id": "native.apple.f9d69f27f9d7f4de", - "source": "Sending...", - "translated": "Wird gesendet..." - }, - { - "id": "native.apple.0c5336129f70faed", - "source": "Retry", - "translated": "Erneut versuchen" - }, - { - "id": "native.apple.7b23f0a89d3ad411", - "source": "Sending approval request", - "translated": "Genehmigungsanfrage wird gesendet" - }, - { - "id": "native.apple.55db7317a2542e51", - "source": "Approval sent", - "translated": "Genehmigung gesendet" - }, - { - "id": "native.apple.06330f62ab254a0e", - "source": "Could not request approval", - "translated": "Genehmigung konnte nicht angefordert werden" - }, - { - "id": "native.apple.6580d3d1cdda682d", - "source": "Enable Talk", - "translated": "Talk aktivieren" - }, - { - "id": "native.apple.abbfc1ba85a230b4", - "source": "Sending a new pairing request to your gateway...", - "translated": "Eine neue Kopplungsanfrage wird an deine Gateway gesendet..." - }, - { - "id": "native.apple.2880deb53ea2fd86", - "source": "Approve this request on your gateway. Talk will start automatically when approval lands.", - "translated": "Genehmige diese Anfrage auf deiner Gateway. Talk startet automatisch, sobald die Genehmigung eintrifft." - }, - { - "id": "native.apple.80faa829f543c03c", - "source": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider.", - "translated": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider." - }, - { - "id": "native.apple.88b0a2e2986fcebf", - "source": "Request Again", - "translated": "Erneut anfordern" - }, - { - "id": "native.apple.5a3faae3472b4b86", - "source": "Send Approval Request", - "translated": "Genehmigungsanfrage senden" - }, { "id": "native.apple.1065cfe92d7c1eb6", "source": "Asking OpenClaw", @@ -22908,11 +22798,6 @@ "source": "Attachments", "translated": "Anhänge" }, - { - "id": "native.apple.f8dda832ed76441d", - "source": "Voice note", - "translated": "Sprachnachricht" - }, { "id": "native.apple.ee2c0d22f5861159", "source": "Talk", @@ -23898,6 +23783,11 @@ "source": "Dismiss", "translated": "Schließen" }, + { + "id": "native.apple.3892b7f55d9d1f0a", + "source": "Remove attachments or wait for delivery to resolve before starting a new chat.", + "translated": "Entfernen Sie Anhänge oder warten Sie auf die Zustellung, bevor Sie einen neuen Chat starten." + }, { "id": "native.apple.92fe9093903eda3b", "source": "Could not attach voice note: %@", @@ -23958,11 +23848,6 @@ "source": "Gateway changed before the session operation started.", "translated": "Das Gateway wurde geändert, bevor der Sitzungsvorgang gestartet wurde." }, - { - "id": "native.apple.d93e6fe16ec1b0e5", - "source": "Remove attachments or wait for delivery to resolve before starting a new chat.", - "translated": "Entfernen Sie Anhänge oder warten Sie auf die Zustellung, bevor Sie einen neuen Chat starten." - }, { "id": "native.apple.710c7bd117a7cc12", "source": "Remove attachments or wait for delivery to resolve before switching chats.", @@ -24188,6 +24073,61 @@ "source": "Compact Session", "translated": "Sitzung komprimieren" }, + { + "id": "native.apple.fa10740f9e272e83", + "source": "Voice note", + "translated": "Sprachnachricht" + }, + { + "id": "native.apple.e4de5afc37981d6c", + "source": "Photo Library", + "translated": "Fotomediathek" + }, + { + "id": "native.apple.37c19a66626b21fa", + "source": "Camera", + "translated": "Kamera" + }, + { + "id": "native.apple.333682684163ba6b", + "source": "Choose Image File", + "translated": "Bilddatei auswählen" + }, + { + "id": "native.apple.534246792690968d", + "source": "Add attachment", + "translated": "Anhang hinzufügen" + }, + { + "id": "native.apple.e33210feaa3da016", + "source": "Record Voice Note", + "translated": "Sprachnotiz aufnehmen" + }, + { + "id": "native.apple.00f0e15858e38d19", + "source": "Dictate message", + "translated": "Nachricht diktieren" + }, + { + "id": "native.apple.96f20175f61a2b29", + "source": "Listening", + "translated": "Hört zu" + }, + { + "id": "native.apple.eaa9047db6cfb67e", + "source": "Not listening", + "translated": "Hört nicht zu" + }, + { + "id": "native.apple.4185df7a940f09ef", + "source": "Finish dictation", + "translated": "Diktat beenden" + }, + { + "id": "native.apple.5a0793d2827b9399", + "source": "Transcribe speech into the message", + "translated": "Sprache in die Nachricht transkribieren." + }, { "id": "native.apple.f74d150ce05205dd", "source": "z", diff --git a/apps/.i18n/native/es.json b/apps/.i18n/native/es.json index 1c9fc9ee4e20..9a63fab3c4f1 100644 --- a/apps/.i18n/native/es.json +++ b/apps/.i18n/native/es.json @@ -8358,11 +8358,21 @@ "source": "OpenClaw Activity", "translated": "Actividad de OpenClaw" }, + { + "id": "native.apple.093081590556db7f", + "source": "OPENCLAW", + "translated": "OPENCLAW" + }, { "id": "native.apple.86d546e1f1247028", "source": "OpenClaw", "translated": "OpenClaw" }, + { + "id": "native.apple.9fd01bdb5a753f3e", + "source": "LIVE", + "translated": "LIVE" + }, { "id": "native.apple.417ac9273b5f63ca", "source": "Connecting...", @@ -8383,6 +8393,36 @@ "source": "Action required", "translated": "Acción requerida" }, + { + "id": "native.apple.275026f681242715", + "source": "Using %@", + "translated": "Usando %@" + }, + { + "id": "native.apple.360d0d0fc0df9eb2", + "source": "Using a tool", + "translated": "Usando una herramienta" + }, + { + "id": "native.apple.0d0ee660962ea840", + "source": "Listening", + "translated": "Escuchando" + }, + { + "id": "native.apple.095ebf1496feb2ad", + "source": "Speaking", + "translated": "Hablando" + }, + { + "id": "native.apple.63c98d2ca6a5b09c", + "source": "Voice active", + "translated": "Voz activada" + }, + { + "id": "native.apple.02470f4b5348f97c", + "source": "Paused", + "translated": "En pausa" + }, { "id": "native.apple.f2f3e6437fafcd1c", "source": "Connected", @@ -10518,11 +10558,36 @@ "source": "The session attaches once the gateway is ready.", "translated": "La sesión se adjunta una vez que el gateway esté listo." }, + { + "id": "native.apple.02611ddddef7295a", + "source": "Collapsed", + "translated": "Contraído" + }, + { + "id": "native.apple.1f4911f4a05230b5", + "source": "Expanded", + "translated": "Expandido" + }, { "id": "native.apple.d19c9577e0fdfea7", "source": "Opens Settings / Gateway", "translated": "Abre Configuración / Gateway" }, + { + "id": "native.apple.b59e690104ae3e24", + "source": "Hides the gateway status label", + "translated": "Oculta la etiqueta de estado de la pasarela" + }, + { + "id": "native.apple.5fcf9d462463dd27", + "source": "Shows the full gateway status label", + "translated": "Muestra la etiqueta completa de estado de la pasarela" + }, + { + "id": "native.apple.592b1e673e42df50", + "source": "Voice off", + "translated": "Voz desactivada" + }, { "id": "native.apple.7c87384c7d1370e1", "source": "New Chat", @@ -10548,6 +10613,11 @@ "source": "Export Transcript", "translated": "Exportar transcripción" }, + { + "id": "native.apple.d510e012c91b24ed", + "source": "Gateway Settings", + "translated": "Ajustes de la pasarela" + }, { "id": "native.apple.f26cef22f417abb4", "source": "Show reasoning & tool activity", @@ -11728,11 +11798,6 @@ "source": "Agent chat and recent work.", "translated": "Chat del agente y trabajo reciente." }, - { - "id": "native.apple.a30d0a950d0a77aa", - "source": "Realtime voice and controls.", - "translated": "Voz y controles en tiempo real." - }, { "id": "native.apple.a03e961ab14ba060", "source": "Overview", @@ -13818,151 +13883,6 @@ "source": "ClawHub did not report an installable version for this skill.", "translated": "ClawHub no indicó una versión instalable para esta skill." }, - { - "id": "native.apple.798c3584ad95f8da", - "source": "Enable Talk", - "translated": "Activar Talk" - }, - { - "id": "native.apple.c6e171128190d131", - "source": "Not Now", - "translated": "Ahora no" - }, - { - "id": "native.apple.e4b15aef3c2e3da7", - "source": "Talk", - "translated": "Hablar" - }, - { - "id": "native.apple.ab3381742bdff650", - "source": "Conversation", - "translated": "Conversación" - }, - { - "id": "native.apple.7af100f6640c6a69", - "source": "Agent", - "translated": "Agente" - }, - { - "id": "native.apple.49c4de60117b1a98", - "source": "Session", - "translated": "Sesión" - }, - { - "id": "native.apple.37c29d2506b955e9", - "source": "Runtime", - "translated": "Runtime" - }, - { - "id": "native.apple.13e0ad79b141576e", - "source": "Voice Mode", - "translated": "Modo de voz" - }, - { - "id": "native.apple.e8035b71bab833c7", - "source": "Configured", - "translated": "Configurado" - }, - { - "id": "native.apple.1b2845ec89d20eef", - "source": "Active", - "translated": "Activo" - }, - { - "id": "native.apple.a82c07df64d13b23", - "source": "Transport", - "translated": "Transporte" - }, - { - "id": "native.apple.384e097278f76954", - "source": "Last issue", - "translated": "Último problema" - }, - { - "id": "native.apple.a6d8fc7bfd98836f", - "source": "Permission", - "translated": "Permiso" - }, - { - "id": "native.apple.983bdcd5e3f1baa8", - "source": "Speech language", - "translated": "Idioma de voz" - }, - { - "id": "native.apple.32e7fa2b1798e9c3", - "source": "Controls", - "translated": "Controles" - }, - { - "id": "native.apple.0eb012a5d3d81b32", - "source": "Speakerphone", - "translated": "Altavoz" - }, - { - "id": "native.apple.ef8699e2718346da", - "source": "Background listening", - "translated": "Escucha en segundo plano" - }, - { - "id": "native.apple.1e6938a09b4fbe77", - "source": "Voice & Talk Settings", - "translated": "Configuración de voz y conversación" - }, - { - "id": "native.apple.c7a591fd5bb6687e", - "source": "Gateway approval is required before this phone can capture voice.", - "translated": "Se requiere la aprobación del Gateway para que este teléfono pueda capturar voz." - }, - { - "id": "native.apple.9d40e75e55ed39eb", - "source": "Voice is disabled in Apple Review demo mode.", - "translated": "La voz está desactivada en el modo de demostración de Apple Review." - }, - { - "id": "native.apple.3ef69bf916177799", - "source": "Connect to your gateway to start a voice conversation.", - "translated": "Conéctate a tu Gateway para iniciar una conversación de voz." - }, - { - "id": "native.apple.ed03af7c68841a5d", - "source": "Open Voice settings after the gateway loads Talk configuration.", - "translated": "Abre los ajustes de voz después de que el Gateway cargue la configuración de Talk." - }, - { - "id": "native.apple.883a251978601b96", - "source": "Routes voice to %@.", - "translated": "Dirige la voz a %@." - }, - { - "id": "native.apple.877fb5c1abfaafa1", - "source": "Not loaded", - "translated": "Not loaded" - }, - { - "id": "native.apple.d3ccb092936551fc", - "source": "Not active", - "translated": "No activo" - }, - { - "id": "native.apple.1620f7cb3deea133", - "source": "Open Gateway Settings", - "translated": "Abrir configuración de Gateway" - }, - { - "id": "native.apple.3fd2706ef0f14fd2", - "source": "Open Voice Settings", - "translated": "Abrir configuración de voz" - }, - { - "id": "native.apple.3cc9cd23a67db077", - "source": "Demo Mode Only", - "translated": "Solo modo demo" - }, - { - "id": "native.apple.12c9d8231777366c", - "source": "Waiting for Approval", - "translated": "Esperando aprobación" - }, { "id": "native.apple.dc57861a3ac5a08e", "source": "Open Settings", @@ -14528,6 +14448,41 @@ "source": "Action required", "translated": "Acción requerida" }, + { + "id": "native.apple.7205f0822706cb55", + "source": "Listening", + "translated": "Escuchando" + }, + { + "id": "native.apple.8b10cb1dc21e42af", + "source": "Listening (PTT)", + "translated": "Escuchando (PTT)" + }, + { + "id": "native.apple.939c7ec55636c67f", + "source": "Listening (Realtime)", + "translated": "Escuchando (en tiempo real)" + }, + { + "id": "native.apple.86f203ebb2453d15", + "source": "Speaking…", + "translated": "Hablando…" + }, + { + "id": "native.apple.1e3ff6b56ae3828d", + "source": "Speaking (System)…", + "translated": "Hablando (sistema)…" + }, + { + "id": "native.apple.37a5fedaefb8ebf3", + "source": "Generating voice…", + "translated": "Generando voz…" + }, + { + "id": "native.apple.47003f094346c660", + "source": "Ready", + "translated": "Listo" + }, { "id": "native.apple.cf0450b542893fb5", "source": "Off", @@ -15348,26 +15303,6 @@ "source": "unknown relay error", "translated": "unknown relay error" }, - { - "id": "native.apple.f2ea327bfea8b8e3", - "source": "Talk", - "translated": "Hablar" - }, - { - "id": "native.apple.bda6cfa988e49a75", - "source": "Control", - "translated": "Control" - }, - { - "id": "native.apple.3c80f5351055a5f3", - "source": "Agent", - "translated": "Agente" - }, - { - "id": "native.apple.22d107db5b905ef4", - "source": "Settings", - "translated": "Configuración" - }, { "id": "native.apple.c23d35de8d70863f", "source": "OpenClaw", @@ -15458,6 +15393,26 @@ "source": "Close canvas", "translated": "Cerrar lienzo" }, + { + "id": "native.apple.a1db6ce0aed1d18e", + "source": "Chat, voice active", + "translated": "Chat, voz activada" + }, + { + "id": "native.apple.bda6cfa988e49a75", + "source": "Control", + "translated": "Control" + }, + { + "id": "native.apple.3c80f5351055a5f3", + "source": "Agent", + "translated": "Agente" + }, + { + "id": "native.apple.22d107db5b905ef4", + "source": "Settings", + "translated": "Configuración" + }, { "id": "native.apple.8a4751c86aa6fe77", "source": "Gateway needs attention", @@ -15488,11 +15443,6 @@ "source": "Chat", "translated": "Chat" }, - { - "id": "native.apple.febca7528af94342", - "source": "Talk", - "translated": "Hablar" - }, { "id": "native.apple.e26bbe303d8a9544", "source": "Overview", @@ -15863,6 +15813,11 @@ "source": "Requesting permissions…", "translated": "Solicitando permisos…" }, + { + "id": "native.apple.198f59c97d7afe11", + "source": "Start failed: %@", + "translated": "Error al iniciar: %@" + }, { "id": "native.apple.a1910b5c2e008b6c", "source": "Microphone permission denied", @@ -15873,11 +15828,6 @@ "source": "Speech recognition", "translated": "Reconocimiento de voz" }, - { - "id": "native.apple.198f59c97d7afe11", - "source": "Start failed: %@", - "translated": "Error al iniciar: %@" - }, { "id": "native.apple.c09f4e3bbef8177a", "source": "Offline", @@ -16088,66 +16038,6 @@ "source": "Listening (PTT)", "translated": "Escuchando (PTT)" }, - { - "id": "native.apple.0189269b6a863dc2", - "source": "Request ID", - "translated": "ID de solicitud" - }, - { - "id": "native.apple.f9d69f27f9d7f4de", - "source": "Sending...", - "translated": "Enviando..." - }, - { - "id": "native.apple.0c5336129f70faed", - "source": "Retry", - "translated": "Reintentar" - }, - { - "id": "native.apple.7b23f0a89d3ad411", - "source": "Sending approval request", - "translated": "Enviando solicitud de aprobación" - }, - { - "id": "native.apple.55db7317a2542e51", - "source": "Approval sent", - "translated": "Aprobación enviada" - }, - { - "id": "native.apple.06330f62ab254a0e", - "source": "Could not request approval", - "translated": "No se pudo solicitar la aprobación" - }, - { - "id": "native.apple.6580d3d1cdda682d", - "source": "Enable Talk", - "translated": "Activar Talk" - }, - { - "id": "native.apple.abbfc1ba85a230b4", - "source": "Sending a new pairing request to your gateway...", - "translated": "Enviando una nueva solicitud de emparejamiento a tu gateway..." - }, - { - "id": "native.apple.2880deb53ea2fd86", - "source": "Approve this request on your gateway. Talk will start automatically when approval lands.", - "translated": "Aprueba esta solicitud en tu gateway. Talk se iniciará automáticamente cuando llegue la aprobación." - }, - { - "id": "native.apple.80faa829f543c03c", - "source": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider.", - "translated": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider." - }, - { - "id": "native.apple.88b0a2e2986fcebf", - "source": "Request Again", - "translated": "Solicitar de nuevo" - }, - { - "id": "native.apple.5a3faae3472b4b86", - "source": "Send Approval Request", - "translated": "Enviar solicitud de aprobación" - }, { "id": "native.apple.1065cfe92d7c1eb6", "source": "Asking OpenClaw", @@ -22908,11 +22798,6 @@ "source": "Attachments", "translated": "Archivos adjuntos" }, - { - "id": "native.apple.f8dda832ed76441d", - "source": "Voice note", - "translated": "Nota de voz" - }, { "id": "native.apple.ee2c0d22f5861159", "source": "Talk", @@ -23898,6 +23783,11 @@ "source": "Dismiss", "translated": "Descartar" }, + { + "id": "native.apple.3892b7f55d9d1f0a", + "source": "Remove attachments or wait for delivery to resolve before starting a new chat.", + "translated": "Elimina los adjuntos o espera a que se complete la entrega antes de iniciar un nuevo chat." + }, { "id": "native.apple.92fe9093903eda3b", "source": "Could not attach voice note: %@", @@ -23958,11 +23848,6 @@ "source": "Gateway changed before the session operation started.", "translated": "El Gateway cambió antes de que se iniciara la operación de sesión." }, - { - "id": "native.apple.d93e6fe16ec1b0e5", - "source": "Remove attachments or wait for delivery to resolve before starting a new chat.", - "translated": "Elimina los adjuntos o espera a que se complete la entrega antes de iniciar un nuevo chat." - }, { "id": "native.apple.710c7bd117a7cc12", "source": "Remove attachments or wait for delivery to resolve before switching chats.", @@ -24188,6 +24073,61 @@ "source": "Compact Session", "translated": "Compactar sesión" }, + { + "id": "native.apple.fa10740f9e272e83", + "source": "Voice note", + "translated": "Nota de voz" + }, + { + "id": "native.apple.e4de5afc37981d6c", + "source": "Photo Library", + "translated": "Fototeca" + }, + { + "id": "native.apple.37c19a66626b21fa", + "source": "Camera", + "translated": "Cámara" + }, + { + "id": "native.apple.333682684163ba6b", + "source": "Choose Image File", + "translated": "Elegir archivo de imagen" + }, + { + "id": "native.apple.534246792690968d", + "source": "Add attachment", + "translated": "Añadir archivo adjunto" + }, + { + "id": "native.apple.e33210feaa3da016", + "source": "Record Voice Note", + "translated": "Grabar nota de voz" + }, + { + "id": "native.apple.00f0e15858e38d19", + "source": "Dictate message", + "translated": "Dictar mensaje" + }, + { + "id": "native.apple.96f20175f61a2b29", + "source": "Listening", + "translated": "Escuchando" + }, + { + "id": "native.apple.eaa9047db6cfb67e", + "source": "Not listening", + "translated": "No está escuchando" + }, + { + "id": "native.apple.4185df7a940f09ef", + "source": "Finish dictation", + "translated": "Finalizar dictado" + }, + { + "id": "native.apple.5a0793d2827b9399", + "source": "Transcribe speech into the message", + "translated": "Transcribe la voz en el mensaje." + }, { "id": "native.apple.f74d150ce05205dd", "source": "z", diff --git a/apps/.i18n/native/fa.json b/apps/.i18n/native/fa.json index 70a4177c0e8a..014319670991 100644 --- a/apps/.i18n/native/fa.json +++ b/apps/.i18n/native/fa.json @@ -8358,11 +8358,21 @@ "source": "OpenClaw Activity", "translated": "فعالیت OpenClaw" }, + { + "id": "native.apple.093081590556db7f", + "source": "OPENCLAW", + "translated": "OPENCLAW" + }, { "id": "native.apple.86d546e1f1247028", "source": "OpenClaw", "translated": "OpenClaw" }, + { + "id": "native.apple.9fd01bdb5a753f3e", + "source": "LIVE", + "translated": "LIVE" + }, { "id": "native.apple.417ac9273b5f63ca", "source": "Connecting...", @@ -8383,6 +8393,36 @@ "source": "Action required", "translated": "اقدام لازم است" }, + { + "id": "native.apple.275026f681242715", + "source": "Using %@", + "translated": "با استفاده از %@" + }, + { + "id": "native.apple.360d0d0fc0df9eb2", + "source": "Using a tool", + "translated": "استفاده از ابزار" + }, + { + "id": "native.apple.0d0ee660962ea840", + "source": "Listening", + "translated": "در حال گوش‌دادن" + }, + { + "id": "native.apple.095ebf1496feb2ad", + "source": "Speaking", + "translated": "در حال گفتار" + }, + { + "id": "native.apple.63c98d2ca6a5b09c", + "source": "Voice active", + "translated": "صدا فعال است" + }, + { + "id": "native.apple.02470f4b5348f97c", + "source": "Paused", + "translated": "متوقف‌شده" + }, { "id": "native.apple.f2f3e6437fafcd1c", "source": "Connected", @@ -10518,11 +10558,36 @@ "source": "The session attaches once the gateway is ready.", "translated": "پس از آماده شدن Gateway، نشست متصل می‌شود." }, + { + "id": "native.apple.02611ddddef7295a", + "source": "Collapsed", + "translated": "جمع‌شده" + }, + { + "id": "native.apple.1f4911f4a05230b5", + "source": "Expanded", + "translated": "بازشده" + }, { "id": "native.apple.d19c9577e0fdfea7", "source": "Opens Settings / Gateway", "translated": "Settings / Gateway را باز می‌کند" }, + { + "id": "native.apple.b59e690104ae3e24", + "source": "Hides the gateway status label", + "translated": "برچسب وضعیت درگاه را پنهان می‌کند" + }, + { + "id": "native.apple.5fcf9d462463dd27", + "source": "Shows the full gateway status label", + "translated": "برچسب کامل وضعیت درگاه را نشان می‌دهد" + }, + { + "id": "native.apple.592b1e673e42df50", + "source": "Voice off", + "translated": "صدا خاموش است" + }, { "id": "native.apple.7c87384c7d1370e1", "source": "New Chat", @@ -10548,6 +10613,11 @@ "source": "Export Transcript", "translated": "خروجی گرفتن از رونوشت" }, + { + "id": "native.apple.d510e012c91b24ed", + "source": "Gateway Settings", + "translated": "تنظیمات درگاه" + }, { "id": "native.apple.f26cef22f417abb4", "source": "Show reasoning & tool activity", @@ -11728,11 +11798,6 @@ "source": "Agent chat and recent work.", "translated": "گفتگوی عامل و کارهای اخیر." }, - { - "id": "native.apple.a30d0a950d0a77aa", - "source": "Realtime voice and controls.", - "translated": "صدای بلادرنگ و کنترل‌ها." - }, { "id": "native.apple.a03e961ab14ba060", "source": "Overview", @@ -13818,151 +13883,6 @@ "source": "ClawHub did not report an installable version for this skill.", "translated": "ClawHub نسخه‌ای قابل نصب برای این مهارت اعلام نکرد." }, - { - "id": "native.apple.798c3584ad95f8da", - "source": "Enable Talk", - "translated": "فعال‌سازی Talk" - }, - { - "id": "native.apple.c6e171128190d131", - "source": "Not Now", - "translated": "فعلاً نه" - }, - { - "id": "native.apple.e4b15aef3c2e3da7", - "source": "Talk", - "translated": "گفت‌وگو" - }, - { - "id": "native.apple.ab3381742bdff650", - "source": "Conversation", - "translated": "مکالمه" - }, - { - "id": "native.apple.7af100f6640c6a69", - "source": "Agent", - "translated": "عامل" - }, - { - "id": "native.apple.49c4de60117b1a98", - "source": "Session", - "translated": "نشست" - }, - { - "id": "native.apple.37c29d2506b955e9", - "source": "Runtime", - "translated": "زمان اجرا" - }, - { - "id": "native.apple.13e0ad79b141576e", - "source": "Voice Mode", - "translated": "حالت صوتی" - }, - { - "id": "native.apple.e8035b71bab833c7", - "source": "Configured", - "translated": "پیکربندی‌شده" - }, - { - "id": "native.apple.1b2845ec89d20eef", - "source": "Active", - "translated": "فعال" - }, - { - "id": "native.apple.a82c07df64d13b23", - "source": "Transport", - "translated": "انتقال" - }, - { - "id": "native.apple.384e097278f76954", - "source": "Last issue", - "translated": "آخرین مشکل" - }, - { - "id": "native.apple.a6d8fc7bfd98836f", - "source": "Permission", - "translated": "مجوز" - }, - { - "id": "native.apple.983bdcd5e3f1baa8", - "source": "Speech language", - "translated": "زبان گفتار" - }, - { - "id": "native.apple.32e7fa2b1798e9c3", - "source": "Controls", - "translated": "کنترل‌ها" - }, - { - "id": "native.apple.0eb012a5d3d81b32", - "source": "Speakerphone", - "translated": "بلندگو" - }, - { - "id": "native.apple.ef8699e2718346da", - "source": "Background listening", - "translated": "گوش‌دادن در پس‌زمینه" - }, - { - "id": "native.apple.1e6938a09b4fbe77", - "source": "Voice & Talk Settings", - "translated": "تنظیمات صدا و گفت‌وگو" - }, - { - "id": "native.apple.c7a591fd5bb6687e", - "source": "Gateway approval is required before this phone can capture voice.", - "translated": "پیش از اینکه این تلفن بتواند صدا را ضبط کند، تأیید Gateway لازم است." - }, - { - "id": "native.apple.9d40e75e55ed39eb", - "source": "Voice is disabled in Apple Review demo mode.", - "translated": "صدا در حالت دموی Apple Review غیرفعال است." - }, - { - "id": "native.apple.3ef69bf916177799", - "source": "Connect to your gateway to start a voice conversation.", - "translated": "برای شروع مکالمه صوتی به Gateway خود متصل شوید." - }, - { - "id": "native.apple.ed03af7c68841a5d", - "source": "Open Voice settings after the gateway loads Talk configuration.", - "translated": "پس از اینکه Gateway پیکربندی Talk را بارگیری کرد، تنظیمات صدا را باز کنید." - }, - { - "id": "native.apple.883a251978601b96", - "source": "Routes voice to %@.", - "translated": "صدا را به %@ هدایت می‌کند." - }, - { - "id": "native.apple.877fb5c1abfaafa1", - "source": "Not loaded", - "translated": "Not loaded" - }, - { - "id": "native.apple.d3ccb092936551fc", - "source": "Not active", - "translated": "غیرفعال" - }, - { - "id": "native.apple.1620f7cb3deea133", - "source": "Open Gateway Settings", - "translated": "باز کردن تنظیمات Gateway" - }, - { - "id": "native.apple.3fd2706ef0f14fd2", - "source": "Open Voice Settings", - "translated": "باز کردن تنظیمات صدا" - }, - { - "id": "native.apple.3cc9cd23a67db077", - "source": "Demo Mode Only", - "translated": "فقط حالت دمو" - }, - { - "id": "native.apple.12c9d8231777366c", - "source": "Waiting for Approval", - "translated": "در انتظار تأیید" - }, { "id": "native.apple.dc57861a3ac5a08e", "source": "Open Settings", @@ -14528,6 +14448,41 @@ "source": "Action required", "translated": "اقدام لازم است" }, + { + "id": "native.apple.7205f0822706cb55", + "source": "Listening", + "translated": "در حال گوش‌دادن" + }, + { + "id": "native.apple.8b10cb1dc21e42af", + "source": "Listening (PTT)", + "translated": "در حال شنیدن (PTT)" + }, + { + "id": "native.apple.939c7ec55636c67f", + "source": "Listening (Realtime)", + "translated": "در حال شنیدن (بلادرنگ)" + }, + { + "id": "native.apple.86f203ebb2453d15", + "source": "Speaking…", + "translated": "در حال صحبت…" + }, + { + "id": "native.apple.1e3ff6b56ae3828d", + "source": "Speaking (System)…", + "translated": "در حال گفتار (سیستم)…" + }, + { + "id": "native.apple.37a5fedaefb8ebf3", + "source": "Generating voice…", + "translated": "در حال تولید صدا…" + }, + { + "id": "native.apple.47003f094346c660", + "source": "Ready", + "translated": "آماده" + }, { "id": "native.apple.cf0450b542893fb5", "source": "Off", @@ -15348,26 +15303,6 @@ "source": "unknown relay error", "translated": "unknown relay error" }, - { - "id": "native.apple.f2ea327bfea8b8e3", - "source": "Talk", - "translated": "گفتگو" - }, - { - "id": "native.apple.bda6cfa988e49a75", - "source": "Control", - "translated": "کنترل" - }, - { - "id": "native.apple.3c80f5351055a5f3", - "source": "Agent", - "translated": "عامل" - }, - { - "id": "native.apple.22d107db5b905ef4", - "source": "Settings", - "translated": "تنظیمات" - }, { "id": "native.apple.c23d35de8d70863f", "source": "OpenClaw", @@ -15458,6 +15393,26 @@ "source": "Close canvas", "translated": "بستن بوم" }, + { + "id": "native.apple.a1db6ce0aed1d18e", + "source": "Chat, voice active", + "translated": "چت، صدا فعال است" + }, + { + "id": "native.apple.bda6cfa988e49a75", + "source": "Control", + "translated": "کنترل" + }, + { + "id": "native.apple.3c80f5351055a5f3", + "source": "Agent", + "translated": "عامل" + }, + { + "id": "native.apple.22d107db5b905ef4", + "source": "Settings", + "translated": "تنظیمات" + }, { "id": "native.apple.8a4751c86aa6fe77", "source": "Gateway needs attention", @@ -15488,11 +15443,6 @@ "source": "Chat", "translated": "گفت‌وگو" }, - { - "id": "native.apple.febca7528af94342", - "source": "Talk", - "translated": "صحبت" - }, { "id": "native.apple.e26bbe303d8a9544", "source": "Overview", @@ -15863,6 +15813,11 @@ "source": "Requesting permissions…", "translated": "در حال درخواست مجوزها…" }, + { + "id": "native.apple.198f59c97d7afe11", + "source": "Start failed: %@", + "translated": "شروع ناموفق بود: %@" + }, { "id": "native.apple.a1910b5c2e008b6c", "source": "Microphone permission denied", @@ -15873,11 +15828,6 @@ "source": "Speech recognition", "translated": "تشخیص گفتار" }, - { - "id": "native.apple.198f59c97d7afe11", - "source": "Start failed: %@", - "translated": "شروع ناموفق بود: %@" - }, { "id": "native.apple.c09f4e3bbef8177a", "source": "Offline", @@ -16088,66 +16038,6 @@ "source": "Listening (PTT)", "translated": "در حال شنیدن (PTT)" }, - { - "id": "native.apple.0189269b6a863dc2", - "source": "Request ID", - "translated": "شناسه درخواست" - }, - { - "id": "native.apple.f9d69f27f9d7f4de", - "source": "Sending...", - "translated": "در حال ارسال..." - }, - { - "id": "native.apple.0c5336129f70faed", - "source": "Retry", - "translated": "تلاش دوباره" - }, - { - "id": "native.apple.7b23f0a89d3ad411", - "source": "Sending approval request", - "translated": "در حال ارسال درخواست تأیید" - }, - { - "id": "native.apple.55db7317a2542e51", - "source": "Approval sent", - "translated": "تأیید ارسال شد" - }, - { - "id": "native.apple.06330f62ab254a0e", - "source": "Could not request approval", - "translated": "امکان درخواست تأیید وجود نداشت" - }, - { - "id": "native.apple.6580d3d1cdda682d", - "source": "Enable Talk", - "translated": "فعال‌سازی Talk" - }, - { - "id": "native.apple.abbfc1ba85a230b4", - "source": "Sending a new pairing request to your gateway...", - "translated": "در حال ارسال درخواست جفت‌سازی جدید به gateway شما..." - }, - { - "id": "native.apple.2880deb53ea2fd86", - "source": "Approve this request on your gateway. Talk will start automatically when approval lands.", - "translated": "این درخواست را در gateway خود تأیید کنید. پس از رسیدن تأیید، Talk به‌طور خودکار شروع می‌شود." - }, - { - "id": "native.apple.80faa829f543c03c", - "source": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider.", - "translated": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider." - }, - { - "id": "native.apple.88b0a2e2986fcebf", - "source": "Request Again", - "translated": "درخواست دوباره" - }, - { - "id": "native.apple.5a3faae3472b4b86", - "source": "Send Approval Request", - "translated": "ارسال درخواست تأیید" - }, { "id": "native.apple.1065cfe92d7c1eb6", "source": "Asking OpenClaw", @@ -22908,11 +22798,6 @@ "source": "Attachments", "translated": "پیوست‌ها" }, - { - "id": "native.apple.f8dda832ed76441d", - "source": "Voice note", - "translated": "یادداشت صوتی" - }, { "id": "native.apple.ee2c0d22f5861159", "source": "Talk", @@ -23898,6 +23783,11 @@ "source": "Dismiss", "translated": "بستن" }, + { + "id": "native.apple.3892b7f55d9d1f0a", + "source": "Remove attachments or wait for delivery to resolve before starting a new chat.", + "translated": "پیش از شروع گفتگوی جدید، پیوست‌ها را حذف کنید یا منتظر تکمیل ارسال بمانید." + }, { "id": "native.apple.92fe9093903eda3b", "source": "Could not attach voice note: %@", @@ -23958,11 +23848,6 @@ "source": "Gateway changed before the session operation started.", "translated": "Gateway پیش از شروع عملیات نشست تغییر کرد." }, - { - "id": "native.apple.d93e6fe16ec1b0e5", - "source": "Remove attachments or wait for delivery to resolve before starting a new chat.", - "translated": "پیش از شروع گفتگوی جدید، پیوست‌ها را حذف کنید یا منتظر تکمیل ارسال بمانید." - }, { "id": "native.apple.710c7bd117a7cc12", "source": "Remove attachments or wait for delivery to resolve before switching chats.", @@ -24188,6 +24073,61 @@ "source": "Compact Session", "translated": "فشرده‌سازی نشست" }, + { + "id": "native.apple.fa10740f9e272e83", + "source": "Voice note", + "translated": "یادداشت صوتی" + }, + { + "id": "native.apple.e4de5afc37981d6c", + "source": "Photo Library", + "translated": "کتابخانه عکس" + }, + { + "id": "native.apple.37c19a66626b21fa", + "source": "Camera", + "translated": "دوربین" + }, + { + "id": "native.apple.333682684163ba6b", + "source": "Choose Image File", + "translated": "انتخاب فایل تصویر" + }, + { + "id": "native.apple.534246792690968d", + "source": "Add attachment", + "translated": "افزودن پیوست" + }, + { + "id": "native.apple.e33210feaa3da016", + "source": "Record Voice Note", + "translated": "ضبط یادداشت صوتی" + }, + { + "id": "native.apple.00f0e15858e38d19", + "source": "Dictate message", + "translated": "دیکته کردن پیام" + }, + { + "id": "native.apple.96f20175f61a2b29", + "source": "Listening", + "translated": "در حال گوش‌دادن" + }, + { + "id": "native.apple.eaa9047db6cfb67e", + "source": "Not listening", + "translated": "در حال گوش دادن نیست" + }, + { + "id": "native.apple.4185df7a940f09ef", + "source": "Finish dictation", + "translated": "پایان دیکته" + }, + { + "id": "native.apple.5a0793d2827b9399", + "source": "Transcribe speech into the message", + "translated": "گفتار را به متن پیام تبدیل کنید." + }, { "id": "native.apple.f74d150ce05205dd", "source": "z", diff --git a/apps/.i18n/native/fr.json b/apps/.i18n/native/fr.json index 55a8a9107017..ffc4d941b87b 100644 --- a/apps/.i18n/native/fr.json +++ b/apps/.i18n/native/fr.json @@ -8358,11 +8358,21 @@ "source": "OpenClaw Activity", "translated": "Activité OpenClaw" }, + { + "id": "native.apple.093081590556db7f", + "source": "OPENCLAW", + "translated": "OPENCLAW" + }, { "id": "native.apple.86d546e1f1247028", "source": "OpenClaw", "translated": "OpenClaw" }, + { + "id": "native.apple.9fd01bdb5a753f3e", + "source": "LIVE", + "translated": "LIVE" + }, { "id": "native.apple.417ac9273b5f63ca", "source": "Connecting...", @@ -8383,6 +8393,36 @@ "source": "Action required", "translated": "Action requise" }, + { + "id": "native.apple.275026f681242715", + "source": "Using %@", + "translated": "Avec %@" + }, + { + "id": "native.apple.360d0d0fc0df9eb2", + "source": "Using a tool", + "translated": "Utilisation d’un outil" + }, + { + "id": "native.apple.0d0ee660962ea840", + "source": "Listening", + "translated": "Écoute" + }, + { + "id": "native.apple.095ebf1496feb2ad", + "source": "Speaking", + "translated": "Lecture en cours" + }, + { + "id": "native.apple.63c98d2ca6a5b09c", + "source": "Voice active", + "translated": "Voix activée" + }, + { + "id": "native.apple.02470f4b5348f97c", + "source": "Paused", + "translated": "En pause" + }, { "id": "native.apple.f2f3e6437fafcd1c", "source": "Connected", @@ -10518,11 +10558,36 @@ "source": "The session attaches once the gateway is ready.", "translated": "La session se connecte une fois que le gateway est prêt." }, + { + "id": "native.apple.02611ddddef7295a", + "source": "Collapsed", + "translated": "Réduit" + }, + { + "id": "native.apple.1f4911f4a05230b5", + "source": "Expanded", + "translated": "Développé" + }, { "id": "native.apple.d19c9577e0fdfea7", "source": "Opens Settings / Gateway", "translated": "Ouvre Réglages / Gateway" }, + { + "id": "native.apple.b59e690104ae3e24", + "source": "Hides the gateway status label", + "translated": "Masque le libellé d’état de la passerelle" + }, + { + "id": "native.apple.5fcf9d462463dd27", + "source": "Shows the full gateway status label", + "translated": "Affiche le libellé d’état complet de la passerelle" + }, + { + "id": "native.apple.592b1e673e42df50", + "source": "Voice off", + "translated": "Voix désactivée" + }, { "id": "native.apple.7c87384c7d1370e1", "source": "New Chat", @@ -10548,6 +10613,11 @@ "source": "Export Transcript", "translated": "Exporter la transcription" }, + { + "id": "native.apple.d510e012c91b24ed", + "source": "Gateway Settings", + "translated": "Réglages de la passerelle" + }, { "id": "native.apple.f26cef22f417abb4", "source": "Show reasoning & tool activity", @@ -11728,11 +11798,6 @@ "source": "Agent chat and recent work.", "translated": "Discussion de l'agent et travaux récents." }, - { - "id": "native.apple.a30d0a950d0a77aa", - "source": "Realtime voice and controls.", - "translated": "Voix et commandes en temps réel." - }, { "id": "native.apple.a03e961ab14ba060", "source": "Overview", @@ -13818,151 +13883,6 @@ "source": "ClawHub did not report an installable version for this skill.", "translated": "ClawHub n’a indiqué aucune version installable pour ce Skill." }, - { - "id": "native.apple.798c3584ad95f8da", - "source": "Enable Talk", - "translated": "Activer Talk" - }, - { - "id": "native.apple.c6e171128190d131", - "source": "Not Now", - "translated": "Pas maintenant" - }, - { - "id": "native.apple.e4b15aef3c2e3da7", - "source": "Talk", - "translated": "Parler" - }, - { - "id": "native.apple.ab3381742bdff650", - "source": "Conversation", - "translated": "Conversation" - }, - { - "id": "native.apple.7af100f6640c6a69", - "source": "Agent", - "translated": "Agent" - }, - { - "id": "native.apple.49c4de60117b1a98", - "source": "Session", - "translated": "Session" - }, - { - "id": "native.apple.37c29d2506b955e9", - "source": "Runtime", - "translated": "Environnement d’exécution" - }, - { - "id": "native.apple.13e0ad79b141576e", - "source": "Voice Mode", - "translated": "Mode vocal" - }, - { - "id": "native.apple.e8035b71bab833c7", - "source": "Configured", - "translated": "Configuré" - }, - { - "id": "native.apple.1b2845ec89d20eef", - "source": "Active", - "translated": "Actif" - }, - { - "id": "native.apple.a82c07df64d13b23", - "source": "Transport", - "translated": "Transport" - }, - { - "id": "native.apple.384e097278f76954", - "source": "Last issue", - "translated": "Dernier problème" - }, - { - "id": "native.apple.a6d8fc7bfd98836f", - "source": "Permission", - "translated": "Autorisation" - }, - { - "id": "native.apple.983bdcd5e3f1baa8", - "source": "Speech language", - "translated": "Langue vocale" - }, - { - "id": "native.apple.32e7fa2b1798e9c3", - "source": "Controls", - "translated": "Contrôles" - }, - { - "id": "native.apple.0eb012a5d3d81b32", - "source": "Speakerphone", - "translated": "Haut-parleur" - }, - { - "id": "native.apple.ef8699e2718346da", - "source": "Background listening", - "translated": "Écoute en arrière-plan" - }, - { - "id": "native.apple.1e6938a09b4fbe77", - "source": "Voice & Talk Settings", - "translated": "Paramètres de voix et de conversation" - }, - { - "id": "native.apple.c7a591fd5bb6687e", - "source": "Gateway approval is required before this phone can capture voice.", - "translated": "L’approbation du Gateway est requise avant que ce téléphone puisse enregistrer la voix." - }, - { - "id": "native.apple.9d40e75e55ed39eb", - "source": "Voice is disabled in Apple Review demo mode.", - "translated": "La voix est désactivée en mode démo Apple Review." - }, - { - "id": "native.apple.3ef69bf916177799", - "source": "Connect to your gateway to start a voice conversation.", - "translated": "Connectez-vous à votre Gateway pour démarrer une conversation vocale." - }, - { - "id": "native.apple.ed03af7c68841a5d", - "source": "Open Voice settings after the gateway loads Talk configuration.", - "translated": "Ouvrez les réglages de la voix une fois que le Gateway a chargé la configuration Talk." - }, - { - "id": "native.apple.883a251978601b96", - "source": "Routes voice to %@.", - "translated": "Achemine la voix vers %@." - }, - { - "id": "native.apple.877fb5c1abfaafa1", - "source": "Not loaded", - "translated": "Not loaded" - }, - { - "id": "native.apple.d3ccb092936551fc", - "source": "Not active", - "translated": "Inactif" - }, - { - "id": "native.apple.1620f7cb3deea133", - "source": "Open Gateway Settings", - "translated": "Ouvrir les paramètres de Gateway" - }, - { - "id": "native.apple.3fd2706ef0f14fd2", - "source": "Open Voice Settings", - "translated": "Ouvrir les paramètres vocaux" - }, - { - "id": "native.apple.3cc9cd23a67db077", - "source": "Demo Mode Only", - "translated": "Mode démo uniquement" - }, - { - "id": "native.apple.12c9d8231777366c", - "source": "Waiting for Approval", - "translated": "En attente d’approbation" - }, { "id": "native.apple.dc57861a3ac5a08e", "source": "Open Settings", @@ -14528,6 +14448,41 @@ "source": "Action required", "translated": "Action requise" }, + { + "id": "native.apple.7205f0822706cb55", + "source": "Listening", + "translated": "Écoute" + }, + { + "id": "native.apple.8b10cb1dc21e42af", + "source": "Listening (PTT)", + "translated": "Écoute (PTT)" + }, + { + "id": "native.apple.939c7ec55636c67f", + "source": "Listening (Realtime)", + "translated": "Écoute en cours (temps réel)" + }, + { + "id": "native.apple.86f203ebb2453d15", + "source": "Speaking…", + "translated": "En train de parler…" + }, + { + "id": "native.apple.1e3ff6b56ae3828d", + "source": "Speaking (System)…", + "translated": "Lecture en cours (système)…" + }, + { + "id": "native.apple.37a5fedaefb8ebf3", + "source": "Generating voice…", + "translated": "Génération de la voix…" + }, + { + "id": "native.apple.47003f094346c660", + "source": "Ready", + "translated": "Prêt" + }, { "id": "native.apple.cf0450b542893fb5", "source": "Off", @@ -15348,26 +15303,6 @@ "source": "unknown relay error", "translated": "unknown relay error" }, - { - "id": "native.apple.f2ea327bfea8b8e3", - "source": "Talk", - "translated": "Parler" - }, - { - "id": "native.apple.bda6cfa988e49a75", - "source": "Control", - "translated": "Contrôle" - }, - { - "id": "native.apple.3c80f5351055a5f3", - "source": "Agent", - "translated": "Agent" - }, - { - "id": "native.apple.22d107db5b905ef4", - "source": "Settings", - "translated": "Réglages" - }, { "id": "native.apple.c23d35de8d70863f", "source": "OpenClaw", @@ -15458,6 +15393,26 @@ "source": "Close canvas", "translated": "Fermer le canevas" }, + { + "id": "native.apple.a1db6ce0aed1d18e", + "source": "Chat, voice active", + "translated": "Chat, voix activée" + }, + { + "id": "native.apple.bda6cfa988e49a75", + "source": "Control", + "translated": "Contrôle" + }, + { + "id": "native.apple.3c80f5351055a5f3", + "source": "Agent", + "translated": "Agent" + }, + { + "id": "native.apple.22d107db5b905ef4", + "source": "Settings", + "translated": "Réglages" + }, { "id": "native.apple.8a4751c86aa6fe77", "source": "Gateway needs attention", @@ -15488,11 +15443,6 @@ "source": "Chat", "translated": "Chat" }, - { - "id": "native.apple.febca7528af94342", - "source": "Talk", - "translated": "Parler" - }, { "id": "native.apple.e26bbe303d8a9544", "source": "Overview", @@ -15863,6 +15813,11 @@ "source": "Requesting permissions…", "translated": "Demande d’autorisations…" }, + { + "id": "native.apple.198f59c97d7afe11", + "source": "Start failed: %@", + "translated": "Échec du démarrage : %@" + }, { "id": "native.apple.a1910b5c2e008b6c", "source": "Microphone permission denied", @@ -15873,11 +15828,6 @@ "source": "Speech recognition", "translated": "Reconnaissance vocale" }, - { - "id": "native.apple.198f59c97d7afe11", - "source": "Start failed: %@", - "translated": "Échec du démarrage : %@" - }, { "id": "native.apple.c09f4e3bbef8177a", "source": "Offline", @@ -16088,66 +16038,6 @@ "source": "Listening (PTT)", "translated": "Écoute (PTT)" }, - { - "id": "native.apple.0189269b6a863dc2", - "source": "Request ID", - "translated": "ID de la demande" - }, - { - "id": "native.apple.f9d69f27f9d7f4de", - "source": "Sending...", - "translated": "Envoi..." - }, - { - "id": "native.apple.0c5336129f70faed", - "source": "Retry", - "translated": "Réessayer" - }, - { - "id": "native.apple.7b23f0a89d3ad411", - "source": "Sending approval request", - "translated": "Envoi de la demande d’approbation" - }, - { - "id": "native.apple.55db7317a2542e51", - "source": "Approval sent", - "translated": "Approbation envoyée" - }, - { - "id": "native.apple.06330f62ab254a0e", - "source": "Could not request approval", - "translated": "Impossible de demander l’approbation" - }, - { - "id": "native.apple.6580d3d1cdda682d", - "source": "Enable Talk", - "translated": "Activer Talk" - }, - { - "id": "native.apple.abbfc1ba85a230b4", - "source": "Sending a new pairing request to your gateway...", - "translated": "Envoi d’une nouvelle demande d’association à votre gateway..." - }, - { - "id": "native.apple.2880deb53ea2fd86", - "source": "Approve this request on your gateway. Talk will start automatically when approval lands.", - "translated": "Approuvez cette demande sur votre gateway. Talk démarrera automatiquement une fois l’approbation reçue." - }, - { - "id": "native.apple.80faa829f543c03c", - "source": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider.", - "translated": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider." - }, - { - "id": "native.apple.88b0a2e2986fcebf", - "source": "Request Again", - "translated": "Demander à nouveau" - }, - { - "id": "native.apple.5a3faae3472b4b86", - "source": "Send Approval Request", - "translated": "Envoyer une demande d’approbation" - }, { "id": "native.apple.1065cfe92d7c1eb6", "source": "Asking OpenClaw", @@ -22908,11 +22798,6 @@ "source": "Attachments", "translated": "Pièces jointes" }, - { - "id": "native.apple.f8dda832ed76441d", - "source": "Voice note", - "translated": "Note vocale" - }, { "id": "native.apple.ee2c0d22f5861159", "source": "Talk", @@ -23898,6 +23783,11 @@ "source": "Dismiss", "translated": "Ignorer" }, + { + "id": "native.apple.3892b7f55d9d1f0a", + "source": "Remove attachments or wait for delivery to resolve before starting a new chat.", + "translated": "Supprimez les pièces jointes ou attendez la livraison avant de démarrer une nouvelle conversation." + }, { "id": "native.apple.92fe9093903eda3b", "source": "Could not attach voice note: %@", @@ -23958,11 +23848,6 @@ "source": "Gateway changed before the session operation started.", "translated": "Le Gateway a changé avant le début de l’opération sur la session." }, - { - "id": "native.apple.d93e6fe16ec1b0e5", - "source": "Remove attachments or wait for delivery to resolve before starting a new chat.", - "translated": "Supprimez les pièces jointes ou attendez la livraison avant de démarrer une nouvelle conversation." - }, { "id": "native.apple.710c7bd117a7cc12", "source": "Remove attachments or wait for delivery to resolve before switching chats.", @@ -24188,6 +24073,61 @@ "source": "Compact Session", "translated": "Compacter la session" }, + { + "id": "native.apple.fa10740f9e272e83", + "source": "Voice note", + "translated": "Note vocale" + }, + { + "id": "native.apple.e4de5afc37981d6c", + "source": "Photo Library", + "translated": "Photothèque" + }, + { + "id": "native.apple.37c19a66626b21fa", + "source": "Camera", + "translated": "Appareil photo" + }, + { + "id": "native.apple.333682684163ba6b", + "source": "Choose Image File", + "translated": "Choisir un fichier image" + }, + { + "id": "native.apple.534246792690968d", + "source": "Add attachment", + "translated": "Ajouter une pièce jointe" + }, + { + "id": "native.apple.e33210feaa3da016", + "source": "Record Voice Note", + "translated": "Enregistrer une note vocale" + }, + { + "id": "native.apple.00f0e15858e38d19", + "source": "Dictate message", + "translated": "Dicter le message" + }, + { + "id": "native.apple.96f20175f61a2b29", + "source": "Listening", + "translated": "Écoute" + }, + { + "id": "native.apple.eaa9047db6cfb67e", + "source": "Not listening", + "translated": "N’écoute pas" + }, + { + "id": "native.apple.4185df7a940f09ef", + "source": "Finish dictation", + "translated": "Terminer la dictée" + }, + { + "id": "native.apple.5a0793d2827b9399", + "source": "Transcribe speech into the message", + "translated": "Transcrire la parole dans le message." + }, { "id": "native.apple.f74d150ce05205dd", "source": "z", diff --git a/apps/.i18n/native/hi.json b/apps/.i18n/native/hi.json index 300c0a1519d5..b7219f1b5c89 100644 --- a/apps/.i18n/native/hi.json +++ b/apps/.i18n/native/hi.json @@ -8358,11 +8358,21 @@ "source": "OpenClaw Activity", "translated": "OpenClaw गतिविधि" }, + { + "id": "native.apple.093081590556db7f", + "source": "OPENCLAW", + "translated": "OPENCLAW" + }, { "id": "native.apple.86d546e1f1247028", "source": "OpenClaw", "translated": "OpenClaw" }, + { + "id": "native.apple.9fd01bdb5a753f3e", + "source": "LIVE", + "translated": "LIVE" + }, { "id": "native.apple.417ac9273b5f63ca", "source": "Connecting...", @@ -8383,6 +8393,36 @@ "source": "Action required", "translated": "कार्रवाई आवश्यक" }, + { + "id": "native.apple.275026f681242715", + "source": "Using %@", + "translated": "%@ का उपयोग करके" + }, + { + "id": "native.apple.360d0d0fc0df9eb2", + "source": "Using a tool", + "translated": "टूल का उपयोग" + }, + { + "id": "native.apple.0d0ee660962ea840", + "source": "Listening", + "translated": "सुना जा रहा है" + }, + { + "id": "native.apple.095ebf1496feb2ad", + "source": "Speaking", + "translated": "बोल रहा है" + }, + { + "id": "native.apple.63c98d2ca6a5b09c", + "source": "Voice active", + "translated": "आवाज़ चालू" + }, + { + "id": "native.apple.02470f4b5348f97c", + "source": "Paused", + "translated": "रोके गए" + }, { "id": "native.apple.f2f3e6437fafcd1c", "source": "Connected", @@ -10518,11 +10558,36 @@ "source": "The session attaches once the gateway is ready.", "translated": "Gateway तैयार होने के बाद सेशन अटैच हो जाता है।" }, + { + "id": "native.apple.02611ddddef7295a", + "source": "Collapsed", + "translated": "संक्षिप्त" + }, + { + "id": "native.apple.1f4911f4a05230b5", + "source": "Expanded", + "translated": "विस्तृत" + }, { "id": "native.apple.d19c9577e0fdfea7", "source": "Opens Settings / Gateway", "translated": "Settings / Gateway खोलता है" }, + { + "id": "native.apple.b59e690104ae3e24", + "source": "Hides the gateway status label", + "translated": "गेटवे स्थिति लेबल छिपाता है" + }, + { + "id": "native.apple.5fcf9d462463dd27", + "source": "Shows the full gateway status label", + "translated": "पूरा गेटवे स्थिति लेबल दिखाता है" + }, + { + "id": "native.apple.592b1e673e42df50", + "source": "Voice off", + "translated": "आवाज़ बंद" + }, { "id": "native.apple.7c87384c7d1370e1", "source": "New Chat", @@ -10548,6 +10613,11 @@ "source": "Export Transcript", "translated": "प्रतिलेख निर्यात करें" }, + { + "id": "native.apple.d510e012c91b24ed", + "source": "Gateway Settings", + "translated": "गेटवे सेटिंग्स" + }, { "id": "native.apple.f26cef22f417abb4", "source": "Show reasoning & tool activity", @@ -11728,11 +11798,6 @@ "source": "Agent chat and recent work.", "translated": "एजेंट चैट और हाल का कार्य।" }, - { - "id": "native.apple.a30d0a950d0a77aa", - "source": "Realtime voice and controls.", - "translated": "रीयलटाइम आवाज़ और नियंत्रण।" - }, { "id": "native.apple.a03e961ab14ba060", "source": "Overview", @@ -13818,151 +13883,6 @@ "source": "ClawHub did not report an installable version for this skill.", "translated": "ClawHub ने इस स्किल के लिए इंस्टॉल करने योग्य संस्करण की जानकारी नहीं दी।" }, - { - "id": "native.apple.798c3584ad95f8da", - "source": "Enable Talk", - "translated": "Talk सक्षम करें" - }, - { - "id": "native.apple.c6e171128190d131", - "source": "Not Now", - "translated": "अभी नहीं" - }, - { - "id": "native.apple.e4b15aef3c2e3da7", - "source": "Talk", - "translated": "बात करें" - }, - { - "id": "native.apple.ab3381742bdff650", - "source": "Conversation", - "translated": "बातचीत" - }, - { - "id": "native.apple.7af100f6640c6a69", - "source": "Agent", - "translated": "एजेंट" - }, - { - "id": "native.apple.49c4de60117b1a98", - "source": "Session", - "translated": "सेशन" - }, - { - "id": "native.apple.37c29d2506b955e9", - "source": "Runtime", - "translated": "रनटाइम" - }, - { - "id": "native.apple.13e0ad79b141576e", - "source": "Voice Mode", - "translated": "वॉइस मोड" - }, - { - "id": "native.apple.e8035b71bab833c7", - "source": "Configured", - "translated": "कॉन्फ़िगर किया गया" - }, - { - "id": "native.apple.1b2845ec89d20eef", - "source": "Active", - "translated": "सक्रिय" - }, - { - "id": "native.apple.a82c07df64d13b23", - "source": "Transport", - "translated": "ट्रांसपोर्ट" - }, - { - "id": "native.apple.384e097278f76954", - "source": "Last issue", - "translated": "पिछली समस्या" - }, - { - "id": "native.apple.a6d8fc7bfd98836f", - "source": "Permission", - "translated": "अनुमति" - }, - { - "id": "native.apple.983bdcd5e3f1baa8", - "source": "Speech language", - "translated": "बोलने की भाषा" - }, - { - "id": "native.apple.32e7fa2b1798e9c3", - "source": "Controls", - "translated": "नियंत्रण" - }, - { - "id": "native.apple.0eb012a5d3d81b32", - "source": "Speakerphone", - "translated": "स्पीकरफ़ोन" - }, - { - "id": "native.apple.ef8699e2718346da", - "source": "Background listening", - "translated": "बैकग्राउंड में सुनना" - }, - { - "id": "native.apple.1e6938a09b4fbe77", - "source": "Voice & Talk Settings", - "translated": "वॉइस और बातचीत सेटिंग्स" - }, - { - "id": "native.apple.c7a591fd5bb6687e", - "source": "Gateway approval is required before this phone can capture voice.", - "translated": "यह फ़ोन वॉइस कैप्चर कर सके, इससे पहले Gateway की मंज़ूरी आवश्यक है।" - }, - { - "id": "native.apple.9d40e75e55ed39eb", - "source": "Voice is disabled in Apple Review demo mode.", - "translated": "Apple Review डेमो मोड में वॉइस अक्षम है।" - }, - { - "id": "native.apple.3ef69bf916177799", - "source": "Connect to your gateway to start a voice conversation.", - "translated": "वॉइस वार्तालाप शुरू करने के लिए अपने Gateway से कनेक्ट करें।" - }, - { - "id": "native.apple.ed03af7c68841a5d", - "source": "Open Voice settings after the gateway loads Talk configuration.", - "translated": "Gateway द्वारा Talk कॉन्फ़िगरेशन लोड किए जाने के बाद वॉइस सेटिंग्स खोलें।" - }, - { - "id": "native.apple.883a251978601b96", - "source": "Routes voice to %@.", - "translated": "वॉइस को %@ पर रूट करता है।" - }, - { - "id": "native.apple.877fb5c1abfaafa1", - "source": "Not loaded", - "translated": "Not loaded" - }, - { - "id": "native.apple.d3ccb092936551fc", - "source": "Not active", - "translated": "सक्रिय नहीं" - }, - { - "id": "native.apple.1620f7cb3deea133", - "source": "Open Gateway Settings", - "translated": "Gateway सेटिंग्स खोलें" - }, - { - "id": "native.apple.3fd2706ef0f14fd2", - "source": "Open Voice Settings", - "translated": "Voice सेटिंग्स खोलें" - }, - { - "id": "native.apple.3cc9cd23a67db077", - "source": "Demo Mode Only", - "translated": "केवल डेमो मोड" - }, - { - "id": "native.apple.12c9d8231777366c", - "source": "Waiting for Approval", - "translated": "स्वीकृति की प्रतीक्षा है" - }, { "id": "native.apple.dc57861a3ac5a08e", "source": "Open Settings", @@ -14528,6 +14448,41 @@ "source": "Action required", "translated": "कार्रवाई आवश्यक" }, + { + "id": "native.apple.7205f0822706cb55", + "source": "Listening", + "translated": "सुना जा रहा है" + }, + { + "id": "native.apple.8b10cb1dc21e42af", + "source": "Listening (PTT)", + "translated": "सुन रहा है (PTT)" + }, + { + "id": "native.apple.939c7ec55636c67f", + "source": "Listening (Realtime)", + "translated": "सुन रहा है (रीयलटाइम)" + }, + { + "id": "native.apple.86f203ebb2453d15", + "source": "Speaking…", + "translated": "बोल रहा है…" + }, + { + "id": "native.apple.1e3ff6b56ae3828d", + "source": "Speaking (System)…", + "translated": "बोल रहा है (सिस्टम)…" + }, + { + "id": "native.apple.37a5fedaefb8ebf3", + "source": "Generating voice…", + "translated": "आवाज़ जनरेट हो रही है…" + }, + { + "id": "native.apple.47003f094346c660", + "source": "Ready", + "translated": "तैयार" + }, { "id": "native.apple.cf0450b542893fb5", "source": "Off", @@ -15348,26 +15303,6 @@ "source": "unknown relay error", "translated": "unknown relay error" }, - { - "id": "native.apple.f2ea327bfea8b8e3", - "source": "Talk", - "translated": "बात करें" - }, - { - "id": "native.apple.bda6cfa988e49a75", - "source": "Control", - "translated": "नियंत्रण" - }, - { - "id": "native.apple.3c80f5351055a5f3", - "source": "Agent", - "translated": "एजेंट" - }, - { - "id": "native.apple.22d107db5b905ef4", - "source": "Settings", - "translated": "सेटिंग्स" - }, { "id": "native.apple.c23d35de8d70863f", "source": "OpenClaw", @@ -15458,6 +15393,26 @@ "source": "Close canvas", "translated": "कैनवास बंद करें" }, + { + "id": "native.apple.a1db6ce0aed1d18e", + "source": "Chat, voice active", + "translated": "चैट, आवाज़ चालू" + }, + { + "id": "native.apple.bda6cfa988e49a75", + "source": "Control", + "translated": "नियंत्रण" + }, + { + "id": "native.apple.3c80f5351055a5f3", + "source": "Agent", + "translated": "एजेंट" + }, + { + "id": "native.apple.22d107db5b905ef4", + "source": "Settings", + "translated": "सेटिंग्स" + }, { "id": "native.apple.8a4751c86aa6fe77", "source": "Gateway needs attention", @@ -15488,11 +15443,6 @@ "source": "Chat", "translated": "चैट" }, - { - "id": "native.apple.febca7528af94342", - "source": "Talk", - "translated": "बात करें" - }, { "id": "native.apple.e26bbe303d8a9544", "source": "Overview", @@ -15863,6 +15813,11 @@ "source": "Requesting permissions…", "translated": "अनुमतियाँ माँगी जा रही हैं…" }, + { + "id": "native.apple.198f59c97d7afe11", + "source": "Start failed: %@", + "translated": "शुरू नहीं हो सका: %@" + }, { "id": "native.apple.a1910b5c2e008b6c", "source": "Microphone permission denied", @@ -15873,11 +15828,6 @@ "source": "Speech recognition", "translated": "स्पीच रिकग्निशन" }, - { - "id": "native.apple.198f59c97d7afe11", - "source": "Start failed: %@", - "translated": "शुरू नहीं हो सका: %@" - }, { "id": "native.apple.c09f4e3bbef8177a", "source": "Offline", @@ -16088,66 +16038,6 @@ "source": "Listening (PTT)", "translated": "सुन रहा है (PTT)" }, - { - "id": "native.apple.0189269b6a863dc2", - "source": "Request ID", - "translated": "अनुरोध ID" - }, - { - "id": "native.apple.f9d69f27f9d7f4de", - "source": "Sending...", - "translated": "भेजा जा रहा है..." - }, - { - "id": "native.apple.0c5336129f70faed", - "source": "Retry", - "translated": "पुनः प्रयास करें" - }, - { - "id": "native.apple.7b23f0a89d3ad411", - "source": "Sending approval request", - "translated": "स्वीकृति अनुरोध भेजा जा रहा है" - }, - { - "id": "native.apple.55db7317a2542e51", - "source": "Approval sent", - "translated": "स्वीकृति भेजी गई" - }, - { - "id": "native.apple.06330f62ab254a0e", - "source": "Could not request approval", - "translated": "स्वीकृति का अनुरोध नहीं किया जा सका" - }, - { - "id": "native.apple.6580d3d1cdda682d", - "source": "Enable Talk", - "translated": "Talk सक्षम करें" - }, - { - "id": "native.apple.abbfc1ba85a230b4", - "source": "Sending a new pairing request to your gateway...", - "translated": "आपके gateway को नया pairing अनुरोध भेजा जा रहा है..." - }, - { - "id": "native.apple.2880deb53ea2fd86", - "source": "Approve this request on your gateway. Talk will start automatically when approval lands.", - "translated": "अपने gateway पर इस अनुरोध को स्वीकृत करें। स्वीकृति मिलते ही Talk अपने-आप शुरू हो जाएगा।" - }, - { - "id": "native.apple.80faa829f543c03c", - "source": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider.", - "translated": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider." - }, - { - "id": "native.apple.88b0a2e2986fcebf", - "source": "Request Again", - "translated": "फिर से अनुरोध करें" - }, - { - "id": "native.apple.5a3faae3472b4b86", - "source": "Send Approval Request", - "translated": "स्वीकृति अनुरोध भेजें" - }, { "id": "native.apple.1065cfe92d7c1eb6", "source": "Asking OpenClaw", @@ -22908,11 +22798,6 @@ "source": "Attachments", "translated": "अटैचमेंट" }, - { - "id": "native.apple.f8dda832ed76441d", - "source": "Voice note", - "translated": "वॉइस नोट" - }, { "id": "native.apple.ee2c0d22f5861159", "source": "Talk", @@ -23898,6 +23783,11 @@ "source": "Dismiss", "translated": "खारिज करें" }, + { + "id": "native.apple.3892b7f55d9d1f0a", + "source": "Remove attachments or wait for delivery to resolve before starting a new chat.", + "translated": "नई चैट शुरू करने से पहले अटैचमेंट हटाएं या डिलीवरी पूरी होने की प्रतीक्षा करें।" + }, { "id": "native.apple.92fe9093903eda3b", "source": "Could not attach voice note: %@", @@ -23958,11 +23848,6 @@ "source": "Gateway changed before the session operation started.", "translated": "सत्र संचालन शुरू होने से पहले Gateway बदल गया।" }, - { - "id": "native.apple.d93e6fe16ec1b0e5", - "source": "Remove attachments or wait for delivery to resolve before starting a new chat.", - "translated": "नई चैट शुरू करने से पहले अटैचमेंट हटाएं या डिलीवरी पूरी होने की प्रतीक्षा करें।" - }, { "id": "native.apple.710c7bd117a7cc12", "source": "Remove attachments or wait for delivery to resolve before switching chats.", @@ -24188,6 +24073,61 @@ "source": "Compact Session", "translated": "सेशन कॉम्पैक्ट करें" }, + { + "id": "native.apple.fa10740f9e272e83", + "source": "Voice note", + "translated": "वॉइस नोट" + }, + { + "id": "native.apple.e4de5afc37981d6c", + "source": "Photo Library", + "translated": "फ़ोटो लाइब्रेरी" + }, + { + "id": "native.apple.37c19a66626b21fa", + "source": "Camera", + "translated": "कैमरा" + }, + { + "id": "native.apple.333682684163ba6b", + "source": "Choose Image File", + "translated": "इमेज फ़ाइल चुनें" + }, + { + "id": "native.apple.534246792690968d", + "source": "Add attachment", + "translated": "अटैचमेंट जोड़ें" + }, + { + "id": "native.apple.e33210feaa3da016", + "source": "Record Voice Note", + "translated": "वॉइस नोट रिकॉर्ड करें" + }, + { + "id": "native.apple.00f0e15858e38d19", + "source": "Dictate message", + "translated": "संदेश बोलकर लिखें" + }, + { + "id": "native.apple.96f20175f61a2b29", + "source": "Listening", + "translated": "सुना जा रहा है" + }, + { + "id": "native.apple.eaa9047db6cfb67e", + "source": "Not listening", + "translated": "नहीं सुन रहा" + }, + { + "id": "native.apple.4185df7a940f09ef", + "source": "Finish dictation", + "translated": "डिक्टेशन समाप्त करें" + }, + { + "id": "native.apple.5a0793d2827b9399", + "source": "Transcribe speech into the message", + "translated": "बोली को संदेश में लिखें।" + }, { "id": "native.apple.f74d150ce05205dd", "source": "z", diff --git a/apps/.i18n/native/id.json b/apps/.i18n/native/id.json index 870fe961ce01..b6b2344704ca 100644 --- a/apps/.i18n/native/id.json +++ b/apps/.i18n/native/id.json @@ -8358,11 +8358,21 @@ "source": "OpenClaw Activity", "translated": "Aktivitas OpenClaw" }, + { + "id": "native.apple.093081590556db7f", + "source": "OPENCLAW", + "translated": "OPENCLAW" + }, { "id": "native.apple.86d546e1f1247028", "source": "OpenClaw", "translated": "OpenClaw" }, + { + "id": "native.apple.9fd01bdb5a753f3e", + "source": "LIVE", + "translated": "LIVE" + }, { "id": "native.apple.417ac9273b5f63ca", "source": "Connecting...", @@ -8383,6 +8393,36 @@ "source": "Action required", "translated": "Tindakan diperlukan" }, + { + "id": "native.apple.275026f681242715", + "source": "Using %@", + "translated": "Menggunakan %@" + }, + { + "id": "native.apple.360d0d0fc0df9eb2", + "source": "Using a tool", + "translated": "Menggunakan alat" + }, + { + "id": "native.apple.0d0ee660962ea840", + "source": "Listening", + "translated": "Mendengarkan" + }, + { + "id": "native.apple.095ebf1496feb2ad", + "source": "Speaking", + "translated": "Sedang berbicara" + }, + { + "id": "native.apple.63c98d2ca6a5b09c", + "source": "Voice active", + "translated": "Suara aktif" + }, + { + "id": "native.apple.02470f4b5348f97c", + "source": "Paused", + "translated": "Dijeda" + }, { "id": "native.apple.f2f3e6437fafcd1c", "source": "Connected", @@ -10518,11 +10558,36 @@ "source": "The session attaches once the gateway is ready.", "translated": "Sesi terhubung setelah gateway siap." }, + { + "id": "native.apple.02611ddddef7295a", + "source": "Collapsed", + "translated": "Ciut" + }, + { + "id": "native.apple.1f4911f4a05230b5", + "source": "Expanded", + "translated": "Buka" + }, { "id": "native.apple.d19c9577e0fdfea7", "source": "Opens Settings / Gateway", "translated": "Membuka Pengaturan / Gateway" }, + { + "id": "native.apple.b59e690104ae3e24", + "source": "Hides the gateway status label", + "translated": "Sembunyikan label status gateway" + }, + { + "id": "native.apple.5fcf9d462463dd27", + "source": "Shows the full gateway status label", + "translated": "Tampilkan label status gateway lengkap" + }, + { + "id": "native.apple.592b1e673e42df50", + "source": "Voice off", + "translated": "Suara mati" + }, { "id": "native.apple.7c87384c7d1370e1", "source": "New Chat", @@ -10548,6 +10613,11 @@ "source": "Export Transcript", "translated": "Ekspor Transkrip" }, + { + "id": "native.apple.d510e012c91b24ed", + "source": "Gateway Settings", + "translated": "Pengaturan Gateway" + }, { "id": "native.apple.f26cef22f417abb4", "source": "Show reasoning & tool activity", @@ -11728,11 +11798,6 @@ "source": "Agent chat and recent work.", "translated": "Obrolan agen dan pekerjaan terkini." }, - { - "id": "native.apple.a30d0a950d0a77aa", - "source": "Realtime voice and controls.", - "translated": "Suara realtime dan kontrol." - }, { "id": "native.apple.a03e961ab14ba060", "source": "Overview", @@ -13818,151 +13883,6 @@ "source": "ClawHub did not report an installable version for this skill.", "translated": "ClawHub tidak melaporkan versi yang dapat diinstal untuk skill ini." }, - { - "id": "native.apple.798c3584ad95f8da", - "source": "Enable Talk", - "translated": "Aktifkan Talk" - }, - { - "id": "native.apple.c6e171128190d131", - "source": "Not Now", - "translated": "Jangan Sekarang" - }, - { - "id": "native.apple.e4b15aef3c2e3da7", - "source": "Talk", - "translated": "Bicara" - }, - { - "id": "native.apple.ab3381742bdff650", - "source": "Conversation", - "translated": "Percakapan" - }, - { - "id": "native.apple.7af100f6640c6a69", - "source": "Agent", - "translated": "Agen" - }, - { - "id": "native.apple.49c4de60117b1a98", - "source": "Session", - "translated": "Sesi" - }, - { - "id": "native.apple.37c29d2506b955e9", - "source": "Runtime", - "translated": "Runtime" - }, - { - "id": "native.apple.13e0ad79b141576e", - "source": "Voice Mode", - "translated": "Mode Suara" - }, - { - "id": "native.apple.e8035b71bab833c7", - "source": "Configured", - "translated": "Dikonfigurasi" - }, - { - "id": "native.apple.1b2845ec89d20eef", - "source": "Active", - "translated": "Aktif" - }, - { - "id": "native.apple.a82c07df64d13b23", - "source": "Transport", - "translated": "Transport" - }, - { - "id": "native.apple.384e097278f76954", - "source": "Last issue", - "translated": "Masalah terakhir" - }, - { - "id": "native.apple.a6d8fc7bfd98836f", - "source": "Permission", - "translated": "Izin" - }, - { - "id": "native.apple.983bdcd5e3f1baa8", - "source": "Speech language", - "translated": "Bahasa ucapan" - }, - { - "id": "native.apple.32e7fa2b1798e9c3", - "source": "Controls", - "translated": "Kontrol" - }, - { - "id": "native.apple.0eb012a5d3d81b32", - "source": "Speakerphone", - "translated": "Speakerphone" - }, - { - "id": "native.apple.ef8699e2718346da", - "source": "Background listening", - "translated": "Mendengarkan di latar belakang" - }, - { - "id": "native.apple.1e6938a09b4fbe77", - "source": "Voice & Talk Settings", - "translated": "Pengaturan Suara & Bicara" - }, - { - "id": "native.apple.c7a591fd5bb6687e", - "source": "Gateway approval is required before this phone can capture voice.", - "translated": "Persetujuan Gateway diperlukan sebelum ponsel ini dapat merekam suara." - }, - { - "id": "native.apple.9d40e75e55ed39eb", - "source": "Voice is disabled in Apple Review demo mode.", - "translated": "Suara dinonaktifkan dalam mode demo Apple Review." - }, - { - "id": "native.apple.3ef69bf916177799", - "source": "Connect to your gateway to start a voice conversation.", - "translated": "Hubungkan ke gateway Anda untuk memulai percakapan suara." - }, - { - "id": "native.apple.ed03af7c68841a5d", - "source": "Open Voice settings after the gateway loads Talk configuration.", - "translated": "Buka pengaturan Suara setelah gateway memuat konfigurasi Talk." - }, - { - "id": "native.apple.883a251978601b96", - "source": "Routes voice to %@.", - "translated": "Merutekan suara ke %@." - }, - { - "id": "native.apple.877fb5c1abfaafa1", - "source": "Not loaded", - "translated": "Not loaded" - }, - { - "id": "native.apple.d3ccb092936551fc", - "source": "Not active", - "translated": "Tidak aktif" - }, - { - "id": "native.apple.1620f7cb3deea133", - "source": "Open Gateway Settings", - "translated": "Buka Pengaturan Gateway" - }, - { - "id": "native.apple.3fd2706ef0f14fd2", - "source": "Open Voice Settings", - "translated": "Buka Pengaturan Suara" - }, - { - "id": "native.apple.3cc9cd23a67db077", - "source": "Demo Mode Only", - "translated": "Hanya Mode Demo" - }, - { - "id": "native.apple.12c9d8231777366c", - "source": "Waiting for Approval", - "translated": "Menunggu Persetujuan" - }, { "id": "native.apple.dc57861a3ac5a08e", "source": "Open Settings", @@ -14528,6 +14448,41 @@ "source": "Action required", "translated": "Tindakan diperlukan" }, + { + "id": "native.apple.7205f0822706cb55", + "source": "Listening", + "translated": "Mendengarkan" + }, + { + "id": "native.apple.8b10cb1dc21e42af", + "source": "Listening (PTT)", + "translated": "Mendengarkan (PTT)" + }, + { + "id": "native.apple.939c7ec55636c67f", + "source": "Listening (Realtime)", + "translated": "Sedang mendengarkan (Waktu Nyata)" + }, + { + "id": "native.apple.86f203ebb2453d15", + "source": "Speaking…", + "translated": "Berbicara…" + }, + { + "id": "native.apple.1e3ff6b56ae3828d", + "source": "Speaking (System)…", + "translated": "Sedang berbicara (Sistem)…" + }, + { + "id": "native.apple.37a5fedaefb8ebf3", + "source": "Generating voice…", + "translated": "Membuat suara…" + }, + { + "id": "native.apple.47003f094346c660", + "source": "Ready", + "translated": "Siap" + }, { "id": "native.apple.cf0450b542893fb5", "source": "Off", @@ -15348,26 +15303,6 @@ "source": "unknown relay error", "translated": "unknown relay error" }, - { - "id": "native.apple.f2ea327bfea8b8e3", - "source": "Talk", - "translated": "Bicara" - }, - { - "id": "native.apple.bda6cfa988e49a75", - "source": "Control", - "translated": "Kontrol" - }, - { - "id": "native.apple.3c80f5351055a5f3", - "source": "Agent", - "translated": "Agen" - }, - { - "id": "native.apple.22d107db5b905ef4", - "source": "Settings", - "translated": "Pengaturan" - }, { "id": "native.apple.c23d35de8d70863f", "source": "OpenClaw", @@ -15458,6 +15393,26 @@ "source": "Close canvas", "translated": "Tutup kanvas" }, + { + "id": "native.apple.a1db6ce0aed1d18e", + "source": "Chat, voice active", + "translated": "Chat, suara aktif" + }, + { + "id": "native.apple.bda6cfa988e49a75", + "source": "Control", + "translated": "Kontrol" + }, + { + "id": "native.apple.3c80f5351055a5f3", + "source": "Agent", + "translated": "Agen" + }, + { + "id": "native.apple.22d107db5b905ef4", + "source": "Settings", + "translated": "Pengaturan" + }, { "id": "native.apple.8a4751c86aa6fe77", "source": "Gateway needs attention", @@ -15488,11 +15443,6 @@ "source": "Chat", "translated": "Chat" }, - { - "id": "native.apple.febca7528af94342", - "source": "Talk", - "translated": "Bicara" - }, { "id": "native.apple.e26bbe303d8a9544", "source": "Overview", @@ -15863,6 +15813,11 @@ "source": "Requesting permissions…", "translated": "Meminta izin…" }, + { + "id": "native.apple.198f59c97d7afe11", + "source": "Start failed: %@", + "translated": "Gagal memulai: %@" + }, { "id": "native.apple.a1910b5c2e008b6c", "source": "Microphone permission denied", @@ -15873,11 +15828,6 @@ "source": "Speech recognition", "translated": "Pengenalan ucapan" }, - { - "id": "native.apple.198f59c97d7afe11", - "source": "Start failed: %@", - "translated": "Gagal memulai: %@" - }, { "id": "native.apple.c09f4e3bbef8177a", "source": "Offline", @@ -16088,66 +16038,6 @@ "source": "Listening (PTT)", "translated": "Mendengarkan (PTT)" }, - { - "id": "native.apple.0189269b6a863dc2", - "source": "Request ID", - "translated": "ID Permintaan" - }, - { - "id": "native.apple.f9d69f27f9d7f4de", - "source": "Sending...", - "translated": "Mengirim..." - }, - { - "id": "native.apple.0c5336129f70faed", - "source": "Retry", - "translated": "Coba lagi" - }, - { - "id": "native.apple.7b23f0a89d3ad411", - "source": "Sending approval request", - "translated": "Mengirim permintaan persetujuan" - }, - { - "id": "native.apple.55db7317a2542e51", - "source": "Approval sent", - "translated": "Persetujuan terkirim" - }, - { - "id": "native.apple.06330f62ab254a0e", - "source": "Could not request approval", - "translated": "Tidak dapat meminta persetujuan" - }, - { - "id": "native.apple.6580d3d1cdda682d", - "source": "Enable Talk", - "translated": "Aktifkan Talk" - }, - { - "id": "native.apple.abbfc1ba85a230b4", - "source": "Sending a new pairing request to your gateway...", - "translated": "Mengirim permintaan penyandingan baru ke gateway Anda..." - }, - { - "id": "native.apple.2880deb53ea2fd86", - "source": "Approve this request on your gateway. Talk will start automatically when approval lands.", - "translated": "Setujui permintaan ini di gateway Anda. Talk akan dimulai otomatis saat persetujuan diterima." - }, - { - "id": "native.apple.80faa829f543c03c", - "source": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider.", - "translated": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider." - }, - { - "id": "native.apple.88b0a2e2986fcebf", - "source": "Request Again", - "translated": "Minta Lagi" - }, - { - "id": "native.apple.5a3faae3472b4b86", - "source": "Send Approval Request", - "translated": "Kirim Permintaan Persetujuan" - }, { "id": "native.apple.1065cfe92d7c1eb6", "source": "Asking OpenClaw", @@ -22908,11 +22798,6 @@ "source": "Attachments", "translated": "Lampiran" }, - { - "id": "native.apple.f8dda832ed76441d", - "source": "Voice note", - "translated": "Catatan suara" - }, { "id": "native.apple.ee2c0d22f5861159", "source": "Talk", @@ -23898,6 +23783,11 @@ "source": "Dismiss", "translated": "Tutup" }, + { + "id": "native.apple.3892b7f55d9d1f0a", + "source": "Remove attachments or wait for delivery to resolve before starting a new chat.", + "translated": "Hapus lampiran atau tunggu pengiriman selesai sebelum memulai obrolan baru." + }, { "id": "native.apple.92fe9093903eda3b", "source": "Could not attach voice note: %@", @@ -23958,11 +23848,6 @@ "source": "Gateway changed before the session operation started.", "translated": "Gateway berubah sebelum operasi sesi dimulai." }, - { - "id": "native.apple.d93e6fe16ec1b0e5", - "source": "Remove attachments or wait for delivery to resolve before starting a new chat.", - "translated": "Hapus lampiran atau tunggu pengiriman selesai sebelum memulai obrolan baru." - }, { "id": "native.apple.710c7bd117a7cc12", "source": "Remove attachments or wait for delivery to resolve before switching chats.", @@ -24188,6 +24073,61 @@ "source": "Compact Session", "translated": "Ringkas Sesi" }, + { + "id": "native.apple.fa10740f9e272e83", + "source": "Voice note", + "translated": "Catatan suara" + }, + { + "id": "native.apple.e4de5afc37981d6c", + "source": "Photo Library", + "translated": "Perpustakaan Foto" + }, + { + "id": "native.apple.37c19a66626b21fa", + "source": "Camera", + "translated": "Kamera" + }, + { + "id": "native.apple.333682684163ba6b", + "source": "Choose Image File", + "translated": "Pilih File Gambar" + }, + { + "id": "native.apple.534246792690968d", + "source": "Add attachment", + "translated": "Tambahkan lampiran" + }, + { + "id": "native.apple.e33210feaa3da016", + "source": "Record Voice Note", + "translated": "Rekam Catatan Suara" + }, + { + "id": "native.apple.00f0e15858e38d19", + "source": "Dictate message", + "translated": "Dikte pesan" + }, + { + "id": "native.apple.96f20175f61a2b29", + "source": "Listening", + "translated": "Mendengarkan" + }, + { + "id": "native.apple.eaa9047db6cfb67e", + "source": "Not listening", + "translated": "Tidak mendengarkan" + }, + { + "id": "native.apple.4185df7a940f09ef", + "source": "Finish dictation", + "translated": "Selesai dikte" + }, + { + "id": "native.apple.5a0793d2827b9399", + "source": "Transcribe speech into the message", + "translated": "Transkripsikan ucapan ke dalam pesan." + }, { "id": "native.apple.f74d150ce05205dd", "source": "z", diff --git a/apps/.i18n/native/it.json b/apps/.i18n/native/it.json index 940e3253f28d..58fbc9e6dc26 100644 --- a/apps/.i18n/native/it.json +++ b/apps/.i18n/native/it.json @@ -8358,11 +8358,21 @@ "source": "OpenClaw Activity", "translated": "Attività OpenClaw" }, + { + "id": "native.apple.093081590556db7f", + "source": "OPENCLAW", + "translated": "OPENCLAW" + }, { "id": "native.apple.86d546e1f1247028", "source": "OpenClaw", "translated": "OpenClaw" }, + { + "id": "native.apple.9fd01bdb5a753f3e", + "source": "LIVE", + "translated": "LIVE" + }, { "id": "native.apple.417ac9273b5f63ca", "source": "Connecting...", @@ -8383,6 +8393,36 @@ "source": "Action required", "translated": "Azione richiesta" }, + { + "id": "native.apple.275026f681242715", + "source": "Using %@", + "translated": "Con %@" + }, + { + "id": "native.apple.360d0d0fc0df9eb2", + "source": "Using a tool", + "translated": "Uso di uno strumento" + }, + { + "id": "native.apple.0d0ee660962ea840", + "source": "Listening", + "translated": "In ascolto" + }, + { + "id": "native.apple.095ebf1496feb2ad", + "source": "Speaking", + "translated": "Riproduzione vocale" + }, + { + "id": "native.apple.63c98d2ca6a5b09c", + "source": "Voice active", + "translated": "Voce attiva" + }, + { + "id": "native.apple.02470f4b5348f97c", + "source": "Paused", + "translated": "In pausa" + }, { "id": "native.apple.f2f3e6437fafcd1c", "source": "Connected", @@ -10518,11 +10558,36 @@ "source": "The session attaches once the gateway is ready.", "translated": "La sessione si collega quando il Gateway è pronto." }, + { + "id": "native.apple.02611ddddef7295a", + "source": "Collapsed", + "translated": "Compresso" + }, + { + "id": "native.apple.1f4911f4a05230b5", + "source": "Expanded", + "translated": "Espanso" + }, { "id": "native.apple.d19c9577e0fdfea7", "source": "Opens Settings / Gateway", "translated": "Apre Impostazioni / Gateway" }, + { + "id": "native.apple.b59e690104ae3e24", + "source": "Hides the gateway status label", + "translated": "Nasconde l’etichetta di stato del gateway" + }, + { + "id": "native.apple.5fcf9d462463dd27", + "source": "Shows the full gateway status label", + "translated": "Mostra l’etichetta completa di stato del gateway" + }, + { + "id": "native.apple.592b1e673e42df50", + "source": "Voice off", + "translated": "Voce disattivata" + }, { "id": "native.apple.7c87384c7d1370e1", "source": "New Chat", @@ -10548,6 +10613,11 @@ "source": "Export Transcript", "translated": "Esporta trascrizione" }, + { + "id": "native.apple.d510e012c91b24ed", + "source": "Gateway Settings", + "translated": "Impostazioni gateway" + }, { "id": "native.apple.f26cef22f417abb4", "source": "Show reasoning & tool activity", @@ -11728,11 +11798,6 @@ "source": "Agent chat and recent work.", "translated": "Chat dell'agente e lavori recenti." }, - { - "id": "native.apple.a30d0a950d0a77aa", - "source": "Realtime voice and controls.", - "translated": "Voce e controlli in tempo reale." - }, { "id": "native.apple.a03e961ab14ba060", "source": "Overview", @@ -13818,151 +13883,6 @@ "source": "ClawHub did not report an installable version for this skill.", "translated": "ClawHub non ha indicato una versione installabile per questa skill." }, - { - "id": "native.apple.798c3584ad95f8da", - "source": "Enable Talk", - "translated": "Abilita Talk" - }, - { - "id": "native.apple.c6e171128190d131", - "source": "Not Now", - "translated": "Non ora" - }, - { - "id": "native.apple.e4b15aef3c2e3da7", - "source": "Talk", - "translated": "Parla" - }, - { - "id": "native.apple.ab3381742bdff650", - "source": "Conversation", - "translated": "Conversazione" - }, - { - "id": "native.apple.7af100f6640c6a69", - "source": "Agent", - "translated": "Agente" - }, - { - "id": "native.apple.49c4de60117b1a98", - "source": "Session", - "translated": "Sessione" - }, - { - "id": "native.apple.37c29d2506b955e9", - "source": "Runtime", - "translated": "Runtime" - }, - { - "id": "native.apple.13e0ad79b141576e", - "source": "Voice Mode", - "translated": "Modalità voce" - }, - { - "id": "native.apple.e8035b71bab833c7", - "source": "Configured", - "translated": "Configurato" - }, - { - "id": "native.apple.1b2845ec89d20eef", - "source": "Active", - "translated": "Attivo" - }, - { - "id": "native.apple.a82c07df64d13b23", - "source": "Transport", - "translated": "Trasporto" - }, - { - "id": "native.apple.384e097278f76954", - "source": "Last issue", - "translated": "Ultimo problema" - }, - { - "id": "native.apple.a6d8fc7bfd98836f", - "source": "Permission", - "translated": "Autorizzazione" - }, - { - "id": "native.apple.983bdcd5e3f1baa8", - "source": "Speech language", - "translated": "Lingua vocale" - }, - { - "id": "native.apple.32e7fa2b1798e9c3", - "source": "Controls", - "translated": "Controlli" - }, - { - "id": "native.apple.0eb012a5d3d81b32", - "source": "Speakerphone", - "translated": "Vivavoce" - }, - { - "id": "native.apple.ef8699e2718346da", - "source": "Background listening", - "translated": "Ascolto in background" - }, - { - "id": "native.apple.1e6938a09b4fbe77", - "source": "Voice & Talk Settings", - "translated": "Impostazioni Voce e conversazione" - }, - { - "id": "native.apple.c7a591fd5bb6687e", - "source": "Gateway approval is required before this phone can capture voice.", - "translated": "È necessaria l'approvazione del Gateway prima che questo telefono possa acquisire la voce." - }, - { - "id": "native.apple.9d40e75e55ed39eb", - "source": "Voice is disabled in Apple Review demo mode.", - "translated": "La voce è disabilitata nella modalità demo per la revisione di Apple." - }, - { - "id": "native.apple.3ef69bf916177799", - "source": "Connect to your gateway to start a voice conversation.", - "translated": "Connettiti al Gateway per avviare una conversazione vocale." - }, - { - "id": "native.apple.ed03af7c68841a5d", - "source": "Open Voice settings after the gateway loads Talk configuration.", - "translated": "Apri le impostazioni Voce dopo che il Gateway ha caricato la configurazione di Talk." - }, - { - "id": "native.apple.883a251978601b96", - "source": "Routes voice to %@.", - "translated": "Instrada la voce verso %@." - }, - { - "id": "native.apple.877fb5c1abfaafa1", - "source": "Not loaded", - "translated": "Not loaded" - }, - { - "id": "native.apple.d3ccb092936551fc", - "source": "Not active", - "translated": "Non attivo" - }, - { - "id": "native.apple.1620f7cb3deea133", - "source": "Open Gateway Settings", - "translated": "Apri impostazioni Gateway" - }, - { - "id": "native.apple.3fd2706ef0f14fd2", - "source": "Open Voice Settings", - "translated": "Apri impostazioni voce" - }, - { - "id": "native.apple.3cc9cd23a67db077", - "source": "Demo Mode Only", - "translated": "Solo modalità demo" - }, - { - "id": "native.apple.12c9d8231777366c", - "source": "Waiting for Approval", - "translated": "In attesa di approvazione" - }, { "id": "native.apple.dc57861a3ac5a08e", "source": "Open Settings", @@ -14528,6 +14448,41 @@ "source": "Action required", "translated": "Azione richiesta" }, + { + "id": "native.apple.7205f0822706cb55", + "source": "Listening", + "translated": "In ascolto" + }, + { + "id": "native.apple.8b10cb1dc21e42af", + "source": "Listening (PTT)", + "translated": "In ascolto (PTT)" + }, + { + "id": "native.apple.939c7ec55636c67f", + "source": "Listening (Realtime)", + "translated": "In ascolto (in tempo reale)" + }, + { + "id": "native.apple.86f203ebb2453d15", + "source": "Speaking…", + "translated": "In riproduzione…" + }, + { + "id": "native.apple.1e3ff6b56ae3828d", + "source": "Speaking (System)…", + "translated": "Riproduzione vocale (Sistema)…" + }, + { + "id": "native.apple.37a5fedaefb8ebf3", + "source": "Generating voice…", + "translated": "Generazione della voce…" + }, + { + "id": "native.apple.47003f094346c660", + "source": "Ready", + "translated": "Pronto" + }, { "id": "native.apple.cf0450b542893fb5", "source": "Off", @@ -15348,26 +15303,6 @@ "source": "unknown relay error", "translated": "unknown relay error" }, - { - "id": "native.apple.f2ea327bfea8b8e3", - "source": "Talk", - "translated": "Parla" - }, - { - "id": "native.apple.bda6cfa988e49a75", - "source": "Control", - "translated": "Controllo" - }, - { - "id": "native.apple.3c80f5351055a5f3", - "source": "Agent", - "translated": "Agente" - }, - { - "id": "native.apple.22d107db5b905ef4", - "source": "Settings", - "translated": "Impostazioni" - }, { "id": "native.apple.c23d35de8d70863f", "source": "OpenClaw", @@ -15458,6 +15393,26 @@ "source": "Close canvas", "translated": "Chiudi canvas" }, + { + "id": "native.apple.a1db6ce0aed1d18e", + "source": "Chat, voice active", + "translated": "Chat, voce attiva" + }, + { + "id": "native.apple.bda6cfa988e49a75", + "source": "Control", + "translated": "Controllo" + }, + { + "id": "native.apple.3c80f5351055a5f3", + "source": "Agent", + "translated": "Agente" + }, + { + "id": "native.apple.22d107db5b905ef4", + "source": "Settings", + "translated": "Impostazioni" + }, { "id": "native.apple.8a4751c86aa6fe77", "source": "Gateway needs attention", @@ -15488,11 +15443,6 @@ "source": "Chat", "translated": "Chat" }, - { - "id": "native.apple.febca7528af94342", - "source": "Talk", - "translated": "Parla" - }, { "id": "native.apple.e26bbe303d8a9544", "source": "Overview", @@ -15863,6 +15813,11 @@ "source": "Requesting permissions…", "translated": "Richiesta delle autorizzazioni…" }, + { + "id": "native.apple.198f59c97d7afe11", + "source": "Start failed: %@", + "translated": "Avvio non riuscito: %@" + }, { "id": "native.apple.a1910b5c2e008b6c", "source": "Microphone permission denied", @@ -15873,11 +15828,6 @@ "source": "Speech recognition", "translated": "Riconoscimento vocale" }, - { - "id": "native.apple.198f59c97d7afe11", - "source": "Start failed: %@", - "translated": "Avvio non riuscito: %@" - }, { "id": "native.apple.c09f4e3bbef8177a", "source": "Offline", @@ -16088,66 +16038,6 @@ "source": "Listening (PTT)", "translated": "In ascolto (PTT)" }, - { - "id": "native.apple.0189269b6a863dc2", - "source": "Request ID", - "translated": "ID richiesta" - }, - { - "id": "native.apple.f9d69f27f9d7f4de", - "source": "Sending...", - "translated": "Invio..." - }, - { - "id": "native.apple.0c5336129f70faed", - "source": "Retry", - "translated": "Riprova" - }, - { - "id": "native.apple.7b23f0a89d3ad411", - "source": "Sending approval request", - "translated": "Invio della richiesta di approvazione" - }, - { - "id": "native.apple.55db7317a2542e51", - "source": "Approval sent", - "translated": "Approvazione inviata" - }, - { - "id": "native.apple.06330f62ab254a0e", - "source": "Could not request approval", - "translated": "Impossibile richiedere l'approvazione" - }, - { - "id": "native.apple.6580d3d1cdda682d", - "source": "Enable Talk", - "translated": "Abilita Talk" - }, - { - "id": "native.apple.abbfc1ba85a230b4", - "source": "Sending a new pairing request to your gateway...", - "translated": "Invio di una nuova richiesta di abbinamento al tuo gateway..." - }, - { - "id": "native.apple.2880deb53ea2fd86", - "source": "Approve this request on your gateway. Talk will start automatically when approval lands.", - "translated": "Approva questa richiesta sul tuo gateway. Talk si avvierà automaticamente quando l'approvazione sarà ricevuta." - }, - { - "id": "native.apple.80faa829f543c03c", - "source": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider.", - "translated": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider." - }, - { - "id": "native.apple.88b0a2e2986fcebf", - "source": "Request Again", - "translated": "Richiedi di nuovo" - }, - { - "id": "native.apple.5a3faae3472b4b86", - "source": "Send Approval Request", - "translated": "Invia richiesta di approvazione" - }, { "id": "native.apple.1065cfe92d7c1eb6", "source": "Asking OpenClaw", @@ -22908,11 +22798,6 @@ "source": "Attachments", "translated": "Allegati" }, - { - "id": "native.apple.f8dda832ed76441d", - "source": "Voice note", - "translated": "Nota vocale" - }, { "id": "native.apple.ee2c0d22f5861159", "source": "Talk", @@ -23898,6 +23783,11 @@ "source": "Dismiss", "translated": "Ignora" }, + { + "id": "native.apple.3892b7f55d9d1f0a", + "source": "Remove attachments or wait for delivery to resolve before starting a new chat.", + "translated": "Rimuovi gli allegati o attendi il completamento della consegna prima di iniziare una nuova chat." + }, { "id": "native.apple.92fe9093903eda3b", "source": "Could not attach voice note: %@", @@ -23958,11 +23848,6 @@ "source": "Gateway changed before the session operation started.", "translated": "Il Gateway è cambiato prima dell'inizio dell'operazione sulla sessione." }, - { - "id": "native.apple.d93e6fe16ec1b0e5", - "source": "Remove attachments or wait for delivery to resolve before starting a new chat.", - "translated": "Rimuovi gli allegati o attendi il completamento della consegna prima di iniziare una nuova chat." - }, { "id": "native.apple.710c7bd117a7cc12", "source": "Remove attachments or wait for delivery to resolve before switching chats.", @@ -24188,6 +24073,61 @@ "source": "Compact Session", "translated": "Comprimi sessione" }, + { + "id": "native.apple.fa10740f9e272e83", + "source": "Voice note", + "translated": "Nota vocale" + }, + { + "id": "native.apple.e4de5afc37981d6c", + "source": "Photo Library", + "translated": "Libreria foto" + }, + { + "id": "native.apple.37c19a66626b21fa", + "source": "Camera", + "translated": "Fotocamera" + }, + { + "id": "native.apple.333682684163ba6b", + "source": "Choose Image File", + "translated": "Scegli file immagine" + }, + { + "id": "native.apple.534246792690968d", + "source": "Add attachment", + "translated": "Aggiungi allegato" + }, + { + "id": "native.apple.e33210feaa3da016", + "source": "Record Voice Note", + "translated": "Registra nota vocale" + }, + { + "id": "native.apple.00f0e15858e38d19", + "source": "Dictate message", + "translated": "Detta messaggio" + }, + { + "id": "native.apple.96f20175f61a2b29", + "source": "Listening", + "translated": "In ascolto" + }, + { + "id": "native.apple.eaa9047db6cfb67e", + "source": "Not listening", + "translated": "Non in ascolto" + }, + { + "id": "native.apple.4185df7a940f09ef", + "source": "Finish dictation", + "translated": "Termina dettatura" + }, + { + "id": "native.apple.5a0793d2827b9399", + "source": "Transcribe speech into the message", + "translated": "Trascrivi il parlato nel messaggio." + }, { "id": "native.apple.f74d150ce05205dd", "source": "z", diff --git a/apps/.i18n/native/ja-JP.json b/apps/.i18n/native/ja-JP.json index 425a169f5ab5..fab8e62b794c 100644 --- a/apps/.i18n/native/ja-JP.json +++ b/apps/.i18n/native/ja-JP.json @@ -8358,11 +8358,21 @@ "source": "OpenClaw Activity", "translated": "OpenClawアクティビティ" }, + { + "id": "native.apple.093081590556db7f", + "source": "OPENCLAW", + "translated": "OPENCLAW" + }, { "id": "native.apple.86d546e1f1247028", "source": "OpenClaw", "translated": "OpenClaw" }, + { + "id": "native.apple.9fd01bdb5a753f3e", + "source": "LIVE", + "translated": "LIVE" + }, { "id": "native.apple.417ac9273b5f63ca", "source": "Connecting...", @@ -8383,6 +8393,36 @@ "source": "Action required", "translated": "対応が必要です" }, + { + "id": "native.apple.275026f681242715", + "source": "Using %@", + "translated": "%@を使用中" + }, + { + "id": "native.apple.360d0d0fc0df9eb2", + "source": "Using a tool", + "translated": "ツールを使用中" + }, + { + "id": "native.apple.0d0ee660962ea840", + "source": "Listening", + "translated": "聞き取り中" + }, + { + "id": "native.apple.095ebf1496feb2ad", + "source": "Speaking", + "translated": "読み上げ中" + }, + { + "id": "native.apple.63c98d2ca6a5b09c", + "source": "Voice active", + "translated": "音声オン" + }, + { + "id": "native.apple.02470f4b5348f97c", + "source": "Paused", + "translated": "一時停止中" + }, { "id": "native.apple.f2f3e6437fafcd1c", "source": "Connected", @@ -10518,11 +10558,36 @@ "source": "The session attaches once the gateway is ready.", "translated": "Gateway の準備ができると、セッションが接続されます。" }, + { + "id": "native.apple.02611ddddef7295a", + "source": "Collapsed", + "translated": "折りたたみ" + }, + { + "id": "native.apple.1f4911f4a05230b5", + "source": "Expanded", + "translated": "展開" + }, { "id": "native.apple.d19c9577e0fdfea7", "source": "Opens Settings / Gateway", "translated": "設定 / Gateway を開きます" }, + { + "id": "native.apple.b59e690104ae3e24", + "source": "Hides the gateway status label", + "translated": "ゲートウェイのステータスラベルを非表示" + }, + { + "id": "native.apple.5fcf9d462463dd27", + "source": "Shows the full gateway status label", + "translated": "ゲートウェイの完全なステータスラベルを表示" + }, + { + "id": "native.apple.592b1e673e42df50", + "source": "Voice off", + "translated": "音声オフ" + }, { "id": "native.apple.7c87384c7d1370e1", "source": "New Chat", @@ -10548,6 +10613,11 @@ "source": "Export Transcript", "translated": "トランスクリプトをエクスポート" }, + { + "id": "native.apple.d510e012c91b24ed", + "source": "Gateway Settings", + "translated": "ゲートウェイ設定" + }, { "id": "native.apple.f26cef22f417abb4", "source": "Show reasoning & tool activity", @@ -11728,11 +11798,6 @@ "source": "Agent chat and recent work.", "translated": "エージェントチャットと最近の作業。" }, - { - "id": "native.apple.a30d0a950d0a77aa", - "source": "Realtime voice and controls.", - "translated": "リアルタイム音声とコントロール。" - }, { "id": "native.apple.a03e961ab14ba060", "source": "Overview", @@ -13818,151 +13883,6 @@ "source": "ClawHub did not report an installable version for this skill.", "translated": "ClawHubから、このスキルのインストール可能なバージョンが報告されませんでした。" }, - { - "id": "native.apple.798c3584ad95f8da", - "source": "Enable Talk", - "translated": "トークを有効化" - }, - { - "id": "native.apple.c6e171128190d131", - "source": "Not Now", - "translated": "今はしない" - }, - { - "id": "native.apple.e4b15aef3c2e3da7", - "source": "Talk", - "translated": "話す" - }, - { - "id": "native.apple.ab3381742bdff650", - "source": "Conversation", - "translated": "会話" - }, - { - "id": "native.apple.7af100f6640c6a69", - "source": "Agent", - "translated": "エージェント" - }, - { - "id": "native.apple.49c4de60117b1a98", - "source": "Session", - "translated": "セッション" - }, - { - "id": "native.apple.37c29d2506b955e9", - "source": "Runtime", - "translated": "ランタイム" - }, - { - "id": "native.apple.13e0ad79b141576e", - "source": "Voice Mode", - "translated": "音声モード" - }, - { - "id": "native.apple.e8035b71bab833c7", - "source": "Configured", - "translated": "構成済み" - }, - { - "id": "native.apple.1b2845ec89d20eef", - "source": "Active", - "translated": "アクティブ" - }, - { - "id": "native.apple.a82c07df64d13b23", - "source": "Transport", - "translated": "トランスポート" - }, - { - "id": "native.apple.384e097278f76954", - "source": "Last issue", - "translated": "最後の問題" - }, - { - "id": "native.apple.a6d8fc7bfd98836f", - "source": "Permission", - "translated": "権限" - }, - { - "id": "native.apple.983bdcd5e3f1baa8", - "source": "Speech language", - "translated": "音声言語" - }, - { - "id": "native.apple.32e7fa2b1798e9c3", - "source": "Controls", - "translated": "コントロール" - }, - { - "id": "native.apple.0eb012a5d3d81b32", - "source": "Speakerphone", - "translated": "スピーカーフォン" - }, - { - "id": "native.apple.ef8699e2718346da", - "source": "Background listening", - "translated": "バックグラウンドでのリスニング" - }, - { - "id": "native.apple.1e6938a09b4fbe77", - "source": "Voice & Talk Settings", - "translated": "音声と会話の設定" - }, - { - "id": "native.apple.c7a591fd5bb6687e", - "source": "Gateway approval is required before this phone can capture voice.", - "translated": "この電話で音声をキャプチャするには、Gatewayの承認が必要です。" - }, - { - "id": "native.apple.9d40e75e55ed39eb", - "source": "Voice is disabled in Apple Review demo mode.", - "translated": "Apple Reviewデモモードでは音声が無効になっています。" - }, - { - "id": "native.apple.3ef69bf916177799", - "source": "Connect to your gateway to start a voice conversation.", - "translated": "音声会話を開始するには、Gatewayに接続してください。" - }, - { - "id": "native.apple.ed03af7c68841a5d", - "source": "Open Voice settings after the gateway loads Talk configuration.", - "translated": "GatewayがTalk設定を読み込んだ後、音声設定を開いてください。" - }, - { - "id": "native.apple.883a251978601b96", - "source": "Routes voice to %@.", - "translated": "音声を%@にルーティングします。" - }, - { - "id": "native.apple.877fb5c1abfaafa1", - "source": "Not loaded", - "translated": "Not loaded" - }, - { - "id": "native.apple.d3ccb092936551fc", - "source": "Not active", - "translated": "非アクティブ" - }, - { - "id": "native.apple.1620f7cb3deea133", - "source": "Open Gateway Settings", - "translated": "Gateway 設定を開く" - }, - { - "id": "native.apple.3fd2706ef0f14fd2", - "source": "Open Voice Settings", - "translated": "音声設定を開く" - }, - { - "id": "native.apple.3cc9cd23a67db077", - "source": "Demo Mode Only", - "translated": "デモモードのみ" - }, - { - "id": "native.apple.12c9d8231777366c", - "source": "Waiting for Approval", - "translated": "承認待ち" - }, { "id": "native.apple.dc57861a3ac5a08e", "source": "Open Settings", @@ -14528,6 +14448,41 @@ "source": "Action required", "translated": "対応が必要です" }, + { + "id": "native.apple.7205f0822706cb55", + "source": "Listening", + "translated": "聞き取り中" + }, + { + "id": "native.apple.8b10cb1dc21e42af", + "source": "Listening (PTT)", + "translated": "聞き取り中(PTT)" + }, + { + "id": "native.apple.939c7ec55636c67f", + "source": "Listening (Realtime)", + "translated": "聞き取り中(リアルタイム)" + }, + { + "id": "native.apple.86f203ebb2453d15", + "source": "Speaking…", + "translated": "発話中…" + }, + { + "id": "native.apple.1e3ff6b56ae3828d", + "source": "Speaking (System)…", + "translated": "読み上げ中(システム)…" + }, + { + "id": "native.apple.37a5fedaefb8ebf3", + "source": "Generating voice…", + "translated": "音声を生成中…" + }, + { + "id": "native.apple.47003f094346c660", + "source": "Ready", + "translated": "準備完了" + }, { "id": "native.apple.cf0450b542893fb5", "source": "Off", @@ -15348,26 +15303,6 @@ "source": "unknown relay error", "translated": "unknown relay error" }, - { - "id": "native.apple.f2ea327bfea8b8e3", - "source": "Talk", - "translated": "会話" - }, - { - "id": "native.apple.bda6cfa988e49a75", - "source": "Control", - "translated": "コントロール" - }, - { - "id": "native.apple.3c80f5351055a5f3", - "source": "Agent", - "translated": "エージェント" - }, - { - "id": "native.apple.22d107db5b905ef4", - "source": "Settings", - "translated": "設定" - }, { "id": "native.apple.c23d35de8d70863f", "source": "OpenClaw", @@ -15458,6 +15393,26 @@ "source": "Close canvas", "translated": "キャンバスを閉じる" }, + { + "id": "native.apple.a1db6ce0aed1d18e", + "source": "Chat, voice active", + "translated": "チャット、音声オン" + }, + { + "id": "native.apple.bda6cfa988e49a75", + "source": "Control", + "translated": "コントロール" + }, + { + "id": "native.apple.3c80f5351055a5f3", + "source": "Agent", + "translated": "エージェント" + }, + { + "id": "native.apple.22d107db5b905ef4", + "source": "Settings", + "translated": "設定" + }, { "id": "native.apple.8a4751c86aa6fe77", "source": "Gateway needs attention", @@ -15488,11 +15443,6 @@ "source": "Chat", "translated": "チャット" }, - { - "id": "native.apple.febca7528af94342", - "source": "Talk", - "translated": "トーク" - }, { "id": "native.apple.e26bbe303d8a9544", "source": "Overview", @@ -15863,6 +15813,11 @@ "source": "Requesting permissions…", "translated": "権限をリクエスト中…" }, + { + "id": "native.apple.198f59c97d7afe11", + "source": "Start failed: %@", + "translated": "開始に失敗しました:%@" + }, { "id": "native.apple.a1910b5c2e008b6c", "source": "Microphone permission denied", @@ -15873,11 +15828,6 @@ "source": "Speech recognition", "translated": "音声認識" }, - { - "id": "native.apple.198f59c97d7afe11", - "source": "Start failed: %@", - "translated": "開始に失敗しました:%@" - }, { "id": "native.apple.c09f4e3bbef8177a", "source": "Offline", @@ -16088,66 +16038,6 @@ "source": "Listening (PTT)", "translated": "聞き取り中(PTT)" }, - { - "id": "native.apple.0189269b6a863dc2", - "source": "Request ID", - "translated": "リクエスト ID" - }, - { - "id": "native.apple.f9d69f27f9d7f4de", - "source": "Sending...", - "translated": "送信中..." - }, - { - "id": "native.apple.0c5336129f70faed", - "source": "Retry", - "translated": "再試行" - }, - { - "id": "native.apple.7b23f0a89d3ad411", - "source": "Sending approval request", - "translated": "承認リクエストを送信中" - }, - { - "id": "native.apple.55db7317a2542e51", - "source": "Approval sent", - "translated": "承認を送信しました" - }, - { - "id": "native.apple.06330f62ab254a0e", - "source": "Could not request approval", - "translated": "承認をリクエストできませんでした" - }, - { - "id": "native.apple.6580d3d1cdda682d", - "source": "Enable Talk", - "translated": "Talk を有効にする" - }, - { - "id": "native.apple.abbfc1ba85a230b4", - "source": "Sending a new pairing request to your gateway...", - "translated": "Gateway に新しいペアリングリクエストを送信しています..." - }, - { - "id": "native.apple.2880deb53ea2fd86", - "source": "Approve this request on your gateway. Talk will start automatically when approval lands.", - "translated": "Gateway でこのリクエストを承認してください。承認されると Talk が自動的に開始します。" - }, - { - "id": "native.apple.80faa829f543c03c", - "source": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider.", - "translated": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider." - }, - { - "id": "native.apple.88b0a2e2986fcebf", - "source": "Request Again", - "translated": "再リクエスト" - }, - { - "id": "native.apple.5a3faae3472b4b86", - "source": "Send Approval Request", - "translated": "承認リクエストを送信" - }, { "id": "native.apple.1065cfe92d7c1eb6", "source": "Asking OpenClaw", @@ -22908,11 +22798,6 @@ "source": "Attachments", "translated": "添付ファイル" }, - { - "id": "native.apple.f8dda832ed76441d", - "source": "Voice note", - "translated": "ボイスメモ" - }, { "id": "native.apple.ee2c0d22f5861159", "source": "Talk", @@ -23898,6 +23783,11 @@ "source": "Dismiss", "translated": "閉じる" }, + { + "id": "native.apple.3892b7f55d9d1f0a", + "source": "Remove attachments or wait for delivery to resolve before starting a new chat.", + "translated": "新しいチャットを開始する前に、添付ファイルを削除するか送信が完了するのを待ってください。" + }, { "id": "native.apple.92fe9093903eda3b", "source": "Could not attach voice note: %@", @@ -23958,11 +23848,6 @@ "source": "Gateway changed before the session operation started.", "translated": "セッション操作を開始する前にGatewayが変更されました。" }, - { - "id": "native.apple.d93e6fe16ec1b0e5", - "source": "Remove attachments or wait for delivery to resolve before starting a new chat.", - "translated": "新しいチャットを開始する前に、添付ファイルを削除するか送信が完了するのを待ってください。" - }, { "id": "native.apple.710c7bd117a7cc12", "source": "Remove attachments or wait for delivery to resolve before switching chats.", @@ -24188,6 +24073,61 @@ "source": "Compact Session", "translated": "セッションを圧縮" }, + { + "id": "native.apple.fa10740f9e272e83", + "source": "Voice note", + "translated": "ボイスノート" + }, + { + "id": "native.apple.e4de5afc37981d6c", + "source": "Photo Library", + "translated": "写真ライブラリ" + }, + { + "id": "native.apple.37c19a66626b21fa", + "source": "Camera", + "translated": "カメラ" + }, + { + "id": "native.apple.333682684163ba6b", + "source": "Choose Image File", + "translated": "画像ファイルを選択" + }, + { + "id": "native.apple.534246792690968d", + "source": "Add attachment", + "translated": "添付を追加" + }, + { + "id": "native.apple.e33210feaa3da016", + "source": "Record Voice Note", + "translated": "ボイスメモを録音" + }, + { + "id": "native.apple.00f0e15858e38d19", + "source": "Dictate message", + "translated": "メッセージを音声入力" + }, + { + "id": "native.apple.96f20175f61a2b29", + "source": "Listening", + "translated": "聞き取り中" + }, + { + "id": "native.apple.eaa9047db6cfb67e", + "source": "Not listening", + "translated": "待機中" + }, + { + "id": "native.apple.4185df7a940f09ef", + "source": "Finish dictation", + "translated": "音声入力を終了" + }, + { + "id": "native.apple.5a0793d2827b9399", + "source": "Transcribe speech into the message", + "translated": "音声をメッセージに文字起こしします。" + }, { "id": "native.apple.f74d150ce05205dd", "source": "z", diff --git a/apps/.i18n/native/ko.json b/apps/.i18n/native/ko.json index 36e93d1153d5..64307bbb381d 100644 --- a/apps/.i18n/native/ko.json +++ b/apps/.i18n/native/ko.json @@ -8358,11 +8358,21 @@ "source": "OpenClaw Activity", "translated": "OpenClaw 활동" }, + { + "id": "native.apple.093081590556db7f", + "source": "OPENCLAW", + "translated": "OPENCLAW" + }, { "id": "native.apple.86d546e1f1247028", "source": "OpenClaw", "translated": "OpenClaw" }, + { + "id": "native.apple.9fd01bdb5a753f3e", + "source": "LIVE", + "translated": "LIVE" + }, { "id": "native.apple.417ac9273b5f63ca", "source": "Connecting...", @@ -8383,6 +8393,36 @@ "source": "Action required", "translated": "작업 필요" }, + { + "id": "native.apple.275026f681242715", + "source": "Using %@", + "translated": "%@ 사용 중" + }, + { + "id": "native.apple.360d0d0fc0df9eb2", + "source": "Using a tool", + "translated": "도구 사용 중" + }, + { + "id": "native.apple.0d0ee660962ea840", + "source": "Listening", + "translated": "듣는 중" + }, + { + "id": "native.apple.095ebf1496feb2ad", + "source": "Speaking", + "translated": "말하는 중" + }, + { + "id": "native.apple.63c98d2ca6a5b09c", + "source": "Voice active", + "translated": "음성 켜짐" + }, + { + "id": "native.apple.02470f4b5348f97c", + "source": "Paused", + "translated": "일시 중지됨" + }, { "id": "native.apple.f2f3e6437fafcd1c", "source": "Connected", @@ -10518,11 +10558,36 @@ "source": "The session attaches once the gateway is ready.", "translated": "Gateway가 준비되면 세션이 연결됩니다." }, + { + "id": "native.apple.02611ddddef7295a", + "source": "Collapsed", + "translated": "접힘" + }, + { + "id": "native.apple.1f4911f4a05230b5", + "source": "Expanded", + "translated": "펼침" + }, { "id": "native.apple.d19c9577e0fdfea7", "source": "Opens Settings / Gateway", "translated": "설정 / Gateway를 엽니다" }, + { + "id": "native.apple.b59e690104ae3e24", + "source": "Hides the gateway status label", + "translated": "게이트웨이 상태 레이블 숨기기" + }, + { + "id": "native.apple.5fcf9d462463dd27", + "source": "Shows the full gateway status label", + "translated": "전체 게이트웨이 상태 레이블 표시" + }, + { + "id": "native.apple.592b1e673e42df50", + "source": "Voice off", + "translated": "음성 꺼짐" + }, { "id": "native.apple.7c87384c7d1370e1", "source": "New Chat", @@ -10548,6 +10613,11 @@ "source": "Export Transcript", "translated": "대화 기록 내보내기" }, + { + "id": "native.apple.d510e012c91b24ed", + "source": "Gateway Settings", + "translated": "게이트웨이 설정" + }, { "id": "native.apple.f26cef22f417abb4", "source": "Show reasoning & tool activity", @@ -11728,11 +11798,6 @@ "source": "Agent chat and recent work.", "translated": "에이전트 채팅 및 최근 작업." }, - { - "id": "native.apple.a30d0a950d0a77aa", - "source": "Realtime voice and controls.", - "translated": "실시간 음성 및 제어." - }, { "id": "native.apple.a03e961ab14ba060", "source": "Overview", @@ -13818,151 +13883,6 @@ "source": "ClawHub did not report an installable version for this skill.", "translated": "ClawHub에서 이 Skill에 설치 가능한 버전을 제공하지 않았습니다." }, - { - "id": "native.apple.798c3584ad95f8da", - "source": "Enable Talk", - "translated": "Talk 활성화" - }, - { - "id": "native.apple.c6e171128190d131", - "source": "Not Now", - "translated": "나중에" - }, - { - "id": "native.apple.e4b15aef3c2e3da7", - "source": "Talk", - "translated": "대화하기" - }, - { - "id": "native.apple.ab3381742bdff650", - "source": "Conversation", - "translated": "대화" - }, - { - "id": "native.apple.7af100f6640c6a69", - "source": "Agent", - "translated": "에이전트" - }, - { - "id": "native.apple.49c4de60117b1a98", - "source": "Session", - "translated": "세션" - }, - { - "id": "native.apple.37c29d2506b955e9", - "source": "Runtime", - "translated": "런타임" - }, - { - "id": "native.apple.13e0ad79b141576e", - "source": "Voice Mode", - "translated": "음성 모드" - }, - { - "id": "native.apple.e8035b71bab833c7", - "source": "Configured", - "translated": "구성됨" - }, - { - "id": "native.apple.1b2845ec89d20eef", - "source": "Active", - "translated": "활성" - }, - { - "id": "native.apple.a82c07df64d13b23", - "source": "Transport", - "translated": "전송" - }, - { - "id": "native.apple.384e097278f76954", - "source": "Last issue", - "translated": "마지막 문제" - }, - { - "id": "native.apple.a6d8fc7bfd98836f", - "source": "Permission", - "translated": "권한" - }, - { - "id": "native.apple.983bdcd5e3f1baa8", - "source": "Speech language", - "translated": "음성 언어" - }, - { - "id": "native.apple.32e7fa2b1798e9c3", - "source": "Controls", - "translated": "컨트롤" - }, - { - "id": "native.apple.0eb012a5d3d81b32", - "source": "Speakerphone", - "translated": "스피커폰" - }, - { - "id": "native.apple.ef8699e2718346da", - "source": "Background listening", - "translated": "백그라운드 듣기" - }, - { - "id": "native.apple.1e6938a09b4fbe77", - "source": "Voice & Talk Settings", - "translated": "음성 및 대화 설정" - }, - { - "id": "native.apple.c7a591fd5bb6687e", - "source": "Gateway approval is required before this phone can capture voice.", - "translated": "이 휴대폰에서 음성을 캡처하려면 Gateway 승인이 필요합니다." - }, - { - "id": "native.apple.9d40e75e55ed39eb", - "source": "Voice is disabled in Apple Review demo mode.", - "translated": "Apple Review 데모 모드에서는 음성이 비활성화됩니다." - }, - { - "id": "native.apple.3ef69bf916177799", - "source": "Connect to your gateway to start a voice conversation.", - "translated": "음성 대화를 시작하려면 Gateway에 연결하세요." - }, - { - "id": "native.apple.ed03af7c68841a5d", - "source": "Open Voice settings after the gateway loads Talk configuration.", - "translated": "Gateway에서 Talk 구성을 불러온 후 음성 설정을 여세요." - }, - { - "id": "native.apple.883a251978601b96", - "source": "Routes voice to %@.", - "translated": "음성을 %@로 라우팅합니다." - }, - { - "id": "native.apple.877fb5c1abfaafa1", - "source": "Not loaded", - "translated": "Not loaded" - }, - { - "id": "native.apple.d3ccb092936551fc", - "source": "Not active", - "translated": "활성 상태 아님" - }, - { - "id": "native.apple.1620f7cb3deea133", - "source": "Open Gateway Settings", - "translated": "Gateway 설정 열기" - }, - { - "id": "native.apple.3fd2706ef0f14fd2", - "source": "Open Voice Settings", - "translated": "음성 설정 열기" - }, - { - "id": "native.apple.3cc9cd23a67db077", - "source": "Demo Mode Only", - "translated": "데모 모드 전용" - }, - { - "id": "native.apple.12c9d8231777366c", - "source": "Waiting for Approval", - "translated": "승인 대기 중" - }, { "id": "native.apple.dc57861a3ac5a08e", "source": "Open Settings", @@ -14528,6 +14448,41 @@ "source": "Action required", "translated": "작업 필요" }, + { + "id": "native.apple.7205f0822706cb55", + "source": "Listening", + "translated": "듣는 중" + }, + { + "id": "native.apple.8b10cb1dc21e42af", + "source": "Listening (PTT)", + "translated": "듣는 중(PTT)" + }, + { + "id": "native.apple.939c7ec55636c67f", + "source": "Listening (Realtime)", + "translated": "듣는 중(실시간)" + }, + { + "id": "native.apple.86f203ebb2453d15", + "source": "Speaking…", + "translated": "말하는 중…" + }, + { + "id": "native.apple.1e3ff6b56ae3828d", + "source": "Speaking (System)…", + "translated": "말하는 중(시스템)…" + }, + { + "id": "native.apple.37a5fedaefb8ebf3", + "source": "Generating voice…", + "translated": "음성 생성 중…" + }, + { + "id": "native.apple.47003f094346c660", + "source": "Ready", + "translated": "준비됨" + }, { "id": "native.apple.cf0450b542893fb5", "source": "Off", @@ -15348,26 +15303,6 @@ "source": "unknown relay error", "translated": "unknown relay error" }, - { - "id": "native.apple.f2ea327bfea8b8e3", - "source": "Talk", - "translated": "대화" - }, - { - "id": "native.apple.bda6cfa988e49a75", - "source": "Control", - "translated": "제어" - }, - { - "id": "native.apple.3c80f5351055a5f3", - "source": "Agent", - "translated": "에이전트" - }, - { - "id": "native.apple.22d107db5b905ef4", - "source": "Settings", - "translated": "설정" - }, { "id": "native.apple.c23d35de8d70863f", "source": "OpenClaw", @@ -15458,6 +15393,26 @@ "source": "Close canvas", "translated": "캔버스 닫기" }, + { + "id": "native.apple.a1db6ce0aed1d18e", + "source": "Chat, voice active", + "translated": "채팅, 음성 켜짐" + }, + { + "id": "native.apple.bda6cfa988e49a75", + "source": "Control", + "translated": "제어" + }, + { + "id": "native.apple.3c80f5351055a5f3", + "source": "Agent", + "translated": "에이전트" + }, + { + "id": "native.apple.22d107db5b905ef4", + "source": "Settings", + "translated": "설정" + }, { "id": "native.apple.8a4751c86aa6fe77", "source": "Gateway needs attention", @@ -15488,11 +15443,6 @@ "source": "Chat", "translated": "채팅" }, - { - "id": "native.apple.febca7528af94342", - "source": "Talk", - "translated": "대화" - }, { "id": "native.apple.e26bbe303d8a9544", "source": "Overview", @@ -15863,6 +15813,11 @@ "source": "Requesting permissions…", "translated": "권한 요청 중…" }, + { + "id": "native.apple.198f59c97d7afe11", + "source": "Start failed: %@", + "translated": "시작 실패: %@" + }, { "id": "native.apple.a1910b5c2e008b6c", "source": "Microphone permission denied", @@ -15873,11 +15828,6 @@ "source": "Speech recognition", "translated": "음성 인식" }, - { - "id": "native.apple.198f59c97d7afe11", - "source": "Start failed: %@", - "translated": "시작 실패: %@" - }, { "id": "native.apple.c09f4e3bbef8177a", "source": "Offline", @@ -16088,66 +16038,6 @@ "source": "Listening (PTT)", "translated": "듣는 중(PTT)" }, - { - "id": "native.apple.0189269b6a863dc2", - "source": "Request ID", - "translated": "요청 ID" - }, - { - "id": "native.apple.f9d69f27f9d7f4de", - "source": "Sending...", - "translated": "전송 중..." - }, - { - "id": "native.apple.0c5336129f70faed", - "source": "Retry", - "translated": "다시 시도" - }, - { - "id": "native.apple.7b23f0a89d3ad411", - "source": "Sending approval request", - "translated": "승인 요청 전송 중" - }, - { - "id": "native.apple.55db7317a2542e51", - "source": "Approval sent", - "translated": "승인이 전송됨" - }, - { - "id": "native.apple.06330f62ab254a0e", - "source": "Could not request approval", - "translated": "승인을 요청할 수 없음" - }, - { - "id": "native.apple.6580d3d1cdda682d", - "source": "Enable Talk", - "translated": "Talk 활성화" - }, - { - "id": "native.apple.abbfc1ba85a230b4", - "source": "Sending a new pairing request to your gateway...", - "translated": "Gateway에 새 페어링 요청을 전송하는 중..." - }, - { - "id": "native.apple.2880deb53ea2fd86", - "source": "Approve this request on your gateway. Talk will start automatically when approval lands.", - "translated": "Gateway에서 이 요청을 승인하세요. 승인이 완료되면 Talk가 자동으로 시작됩니다." - }, - { - "id": "native.apple.80faa829f543c03c", - "source": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider.", - "translated": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider." - }, - { - "id": "native.apple.88b0a2e2986fcebf", - "source": "Request Again", - "translated": "다시 요청" - }, - { - "id": "native.apple.5a3faae3472b4b86", - "source": "Send Approval Request", - "translated": "승인 요청 보내기" - }, { "id": "native.apple.1065cfe92d7c1eb6", "source": "Asking OpenClaw", @@ -22908,11 +22798,6 @@ "source": "Attachments", "translated": "첨부 파일" }, - { - "id": "native.apple.f8dda832ed76441d", - "source": "Voice note", - "translated": "음성 메모" - }, { "id": "native.apple.ee2c0d22f5861159", "source": "Talk", @@ -23898,6 +23783,11 @@ "source": "Dismiss", "translated": "닫기" }, + { + "id": "native.apple.3892b7f55d9d1f0a", + "source": "Remove attachments or wait for delivery to resolve before starting a new chat.", + "translated": "새 채팅을 시작하기 전에 첨부 파일을 제거하거나 전송이 완료될 때까지 기다리세요." + }, { "id": "native.apple.92fe9093903eda3b", "source": "Could not attach voice note: %@", @@ -23958,11 +23848,6 @@ "source": "Gateway changed before the session operation started.", "translated": "세션 작업이 시작되기 전에 Gateway가 변경되었습니다." }, - { - "id": "native.apple.d93e6fe16ec1b0e5", - "source": "Remove attachments or wait for delivery to resolve before starting a new chat.", - "translated": "새 채팅을 시작하기 전에 첨부 파일을 제거하거나 전송이 완료될 때까지 기다리세요." - }, { "id": "native.apple.710c7bd117a7cc12", "source": "Remove attachments or wait for delivery to resolve before switching chats.", @@ -24188,6 +24073,61 @@ "source": "Compact Session", "translated": "세션 압축" }, + { + "id": "native.apple.fa10740f9e272e83", + "source": "Voice note", + "translated": "음성 메모" + }, + { + "id": "native.apple.e4de5afc37981d6c", + "source": "Photo Library", + "translated": "사진 보관함" + }, + { + "id": "native.apple.37c19a66626b21fa", + "source": "Camera", + "translated": "카메라" + }, + { + "id": "native.apple.333682684163ba6b", + "source": "Choose Image File", + "translated": "이미지 파일 선택" + }, + { + "id": "native.apple.534246792690968d", + "source": "Add attachment", + "translated": "첨부 추가" + }, + { + "id": "native.apple.e33210feaa3da016", + "source": "Record Voice Note", + "translated": "음성 메모 녹음" + }, + { + "id": "native.apple.00f0e15858e38d19", + "source": "Dictate message", + "translated": "메시지 받아쓰기" + }, + { + "id": "native.apple.96f20175f61a2b29", + "source": "Listening", + "translated": "듣는 중" + }, + { + "id": "native.apple.eaa9047db6cfb67e", + "source": "Not listening", + "translated": "듣는 중 아님" + }, + { + "id": "native.apple.4185df7a940f09ef", + "source": "Finish dictation", + "translated": "받아쓰기 종료" + }, + { + "id": "native.apple.5a0793d2827b9399", + "source": "Transcribe speech into the message", + "translated": "음성을 메시지에 텍스트로 입력합니다." + }, { "id": "native.apple.f74d150ce05205dd", "source": "z", diff --git a/apps/.i18n/native/nl.json b/apps/.i18n/native/nl.json index f2745301e668..ce006388890f 100644 --- a/apps/.i18n/native/nl.json +++ b/apps/.i18n/native/nl.json @@ -8358,11 +8358,21 @@ "source": "OpenClaw Activity", "translated": "OpenClaw-activiteit" }, + { + "id": "native.apple.093081590556db7f", + "source": "OPENCLAW", + "translated": "OPENCLAW" + }, { "id": "native.apple.86d546e1f1247028", "source": "OpenClaw", "translated": "OpenClaw" }, + { + "id": "native.apple.9fd01bdb5a753f3e", + "source": "LIVE", + "translated": "LIVE" + }, { "id": "native.apple.417ac9273b5f63ca", "source": "Connecting...", @@ -8383,6 +8393,36 @@ "source": "Action required", "translated": "Actie vereist" }, + { + "id": "native.apple.275026f681242715", + "source": "Using %@", + "translated": "Met %@" + }, + { + "id": "native.apple.360d0d0fc0df9eb2", + "source": "Using a tool", + "translated": "Tool gebruiken" + }, + { + "id": "native.apple.0d0ee660962ea840", + "source": "Listening", + "translated": "Luisteren" + }, + { + "id": "native.apple.095ebf1496feb2ad", + "source": "Speaking", + "translated": "Bezig met spreken" + }, + { + "id": "native.apple.63c98d2ca6a5b09c", + "source": "Voice active", + "translated": "Stem actief" + }, + { + "id": "native.apple.02470f4b5348f97c", + "source": "Paused", + "translated": "Gepauzeerd" + }, { "id": "native.apple.f2f3e6437fafcd1c", "source": "Connected", @@ -10518,11 +10558,36 @@ "source": "The session attaches once the gateway is ready.", "translated": "De sessie wordt gekoppeld zodra de Gateway gereed is." }, + { + "id": "native.apple.02611ddddef7295a", + "source": "Collapsed", + "translated": "Ingeklapt" + }, + { + "id": "native.apple.1f4911f4a05230b5", + "source": "Expanded", + "translated": "Uitgeklapt" + }, { "id": "native.apple.d19c9577e0fdfea7", "source": "Opens Settings / Gateway", "translated": "Opent Instellingen / Gateway" }, + { + "id": "native.apple.b59e690104ae3e24", + "source": "Hides the gateway status label", + "translated": "Verbergt het gateway-statuslabel" + }, + { + "id": "native.apple.5fcf9d462463dd27", + "source": "Shows the full gateway status label", + "translated": "Toont het volledige gateway-statuslabel" + }, + { + "id": "native.apple.592b1e673e42df50", + "source": "Voice off", + "translated": "Stem uit" + }, { "id": "native.apple.7c87384c7d1370e1", "source": "New Chat", @@ -10548,6 +10613,11 @@ "source": "Export Transcript", "translated": "Transcript exporteren" }, + { + "id": "native.apple.d510e012c91b24ed", + "source": "Gateway Settings", + "translated": "Gateway-instellingen" + }, { "id": "native.apple.f26cef22f417abb4", "source": "Show reasoning & tool activity", @@ -11728,11 +11798,6 @@ "source": "Agent chat and recent work.", "translated": "Agentchat en recent werk." }, - { - "id": "native.apple.a30d0a950d0a77aa", - "source": "Realtime voice and controls.", - "translated": "Realtime spraak en bediening." - }, { "id": "native.apple.a03e961ab14ba060", "source": "Overview", @@ -13818,151 +13883,6 @@ "source": "ClawHub did not report an installable version for this skill.", "translated": "ClawHub heeft geen installeerbare versie voor deze skill gemeld." }, - { - "id": "native.apple.798c3584ad95f8da", - "source": "Enable Talk", - "translated": "Gesprek inschakelen" - }, - { - "id": "native.apple.c6e171128190d131", - "source": "Not Now", - "translated": "Niet nu" - }, - { - "id": "native.apple.e4b15aef3c2e3da7", - "source": "Talk", - "translated": "Praten" - }, - { - "id": "native.apple.ab3381742bdff650", - "source": "Conversation", - "translated": "Gesprek" - }, - { - "id": "native.apple.7af100f6640c6a69", - "source": "Agent", - "translated": "Agent" - }, - { - "id": "native.apple.49c4de60117b1a98", - "source": "Session", - "translated": "Sessie" - }, - { - "id": "native.apple.37c29d2506b955e9", - "source": "Runtime", - "translated": "Runtime" - }, - { - "id": "native.apple.13e0ad79b141576e", - "source": "Voice Mode", - "translated": "Spraakmodus" - }, - { - "id": "native.apple.e8035b71bab833c7", - "source": "Configured", - "translated": "Geconfigureerd" - }, - { - "id": "native.apple.1b2845ec89d20eef", - "source": "Active", - "translated": "Actief" - }, - { - "id": "native.apple.a82c07df64d13b23", - "source": "Transport", - "translated": "Transport" - }, - { - "id": "native.apple.384e097278f76954", - "source": "Last issue", - "translated": "Laatste probleem" - }, - { - "id": "native.apple.a6d8fc7bfd98836f", - "source": "Permission", - "translated": "Toestemming" - }, - { - "id": "native.apple.983bdcd5e3f1baa8", - "source": "Speech language", - "translated": "Spraaktaal" - }, - { - "id": "native.apple.32e7fa2b1798e9c3", - "source": "Controls", - "translated": "Bediening" - }, - { - "id": "native.apple.0eb012a5d3d81b32", - "source": "Speakerphone", - "translated": "Luidspreker" - }, - { - "id": "native.apple.ef8699e2718346da", - "source": "Background listening", - "translated": "Luisteren op de achtergrond" - }, - { - "id": "native.apple.1e6938a09b4fbe77", - "source": "Voice & Talk Settings", - "translated": "Instellingen voor spraak en praten" - }, - { - "id": "native.apple.c7a591fd5bb6687e", - "source": "Gateway approval is required before this phone can capture voice.", - "translated": "Goedkeuring van de gateway is vereist voordat deze telefoon spraak kan opnemen." - }, - { - "id": "native.apple.9d40e75e55ed39eb", - "source": "Voice is disabled in Apple Review demo mode.", - "translated": "Spraak is uitgeschakeld in de Apple Review-demomodus." - }, - { - "id": "native.apple.3ef69bf916177799", - "source": "Connect to your gateway to start a voice conversation.", - "translated": "Maak verbinding met je gateway om een spraakgesprek te starten." - }, - { - "id": "native.apple.ed03af7c68841a5d", - "source": "Open Voice settings after the gateway loads Talk configuration.", - "translated": "Open de spraakinstellingen nadat de gateway de Talk-configuratie heeft geladen." - }, - { - "id": "native.apple.883a251978601b96", - "source": "Routes voice to %@.", - "translated": "Leidt spraak door naar %@." - }, - { - "id": "native.apple.877fb5c1abfaafa1", - "source": "Not loaded", - "translated": "Not loaded" - }, - { - "id": "native.apple.d3ccb092936551fc", - "source": "Not active", - "translated": "Niet actief" - }, - { - "id": "native.apple.1620f7cb3deea133", - "source": "Open Gateway Settings", - "translated": "Gateway-instellingen openen" - }, - { - "id": "native.apple.3fd2706ef0f14fd2", - "source": "Open Voice Settings", - "translated": "Spraakinstellingen openen" - }, - { - "id": "native.apple.3cc9cd23a67db077", - "source": "Demo Mode Only", - "translated": "Alleen demomodus" - }, - { - "id": "native.apple.12c9d8231777366c", - "source": "Waiting for Approval", - "translated": "Wachten op goedkeuring" - }, { "id": "native.apple.dc57861a3ac5a08e", "source": "Open Settings", @@ -14528,6 +14448,41 @@ "source": "Action required", "translated": "Actie vereist" }, + { + "id": "native.apple.7205f0822706cb55", + "source": "Listening", + "translated": "Luisteren" + }, + { + "id": "native.apple.8b10cb1dc21e42af", + "source": "Listening (PTT)", + "translated": "Luisteren (PTT)" + }, + { + "id": "native.apple.939c7ec55636c67f", + "source": "Listening (Realtime)", + "translated": "Bezig met luisteren (realtime)" + }, + { + "id": "native.apple.86f203ebb2453d15", + "source": "Speaking…", + "translated": "Aan het spreken…" + }, + { + "id": "native.apple.1e3ff6b56ae3828d", + "source": "Speaking (System)…", + "translated": "Bezig met spreken (systeem)…" + }, + { + "id": "native.apple.37a5fedaefb8ebf3", + "source": "Generating voice…", + "translated": "Spraak genereren…" + }, + { + "id": "native.apple.47003f094346c660", + "source": "Ready", + "translated": "Gereed" + }, { "id": "native.apple.cf0450b542893fb5", "source": "Off", @@ -15348,26 +15303,6 @@ "source": "unknown relay error", "translated": "unknown relay error" }, - { - "id": "native.apple.f2ea327bfea8b8e3", - "source": "Talk", - "translated": "Praten" - }, - { - "id": "native.apple.bda6cfa988e49a75", - "source": "Control", - "translated": "Bediening" - }, - { - "id": "native.apple.3c80f5351055a5f3", - "source": "Agent", - "translated": "Agent" - }, - { - "id": "native.apple.22d107db5b905ef4", - "source": "Settings", - "translated": "Instellingen" - }, { "id": "native.apple.c23d35de8d70863f", "source": "OpenClaw", @@ -15458,6 +15393,26 @@ "source": "Close canvas", "translated": "Canvas sluiten" }, + { + "id": "native.apple.a1db6ce0aed1d18e", + "source": "Chat, voice active", + "translated": "Chat, stem actief" + }, + { + "id": "native.apple.bda6cfa988e49a75", + "source": "Control", + "translated": "Bediening" + }, + { + "id": "native.apple.3c80f5351055a5f3", + "source": "Agent", + "translated": "Agent" + }, + { + "id": "native.apple.22d107db5b905ef4", + "source": "Settings", + "translated": "Instellingen" + }, { "id": "native.apple.8a4751c86aa6fe77", "source": "Gateway needs attention", @@ -15488,11 +15443,6 @@ "source": "Chat", "translated": "Chat" }, - { - "id": "native.apple.febca7528af94342", - "source": "Talk", - "translated": "Praten" - }, { "id": "native.apple.e26bbe303d8a9544", "source": "Overview", @@ -15863,6 +15813,11 @@ "source": "Requesting permissions…", "translated": "Toestemmingen aanvragen…" }, + { + "id": "native.apple.198f59c97d7afe11", + "source": "Start failed: %@", + "translated": "Starten mislukt: %@" + }, { "id": "native.apple.a1910b5c2e008b6c", "source": "Microphone permission denied", @@ -15873,11 +15828,6 @@ "source": "Speech recognition", "translated": "Spraakherkenning" }, - { - "id": "native.apple.198f59c97d7afe11", - "source": "Start failed: %@", - "translated": "Starten mislukt: %@" - }, { "id": "native.apple.c09f4e3bbef8177a", "source": "Offline", @@ -16088,66 +16038,6 @@ "source": "Listening (PTT)", "translated": "Luisteren (PTT)" }, - { - "id": "native.apple.0189269b6a863dc2", - "source": "Request ID", - "translated": "Aanvraag-ID" - }, - { - "id": "native.apple.f9d69f27f9d7f4de", - "source": "Sending...", - "translated": "Verzenden..." - }, - { - "id": "native.apple.0c5336129f70faed", - "source": "Retry", - "translated": "Opnieuw proberen" - }, - { - "id": "native.apple.7b23f0a89d3ad411", - "source": "Sending approval request", - "translated": "Goedkeuringsaanvraag verzenden" - }, - { - "id": "native.apple.55db7317a2542e51", - "source": "Approval sent", - "translated": "Goedkeuring verzonden" - }, - { - "id": "native.apple.06330f62ab254a0e", - "source": "Could not request approval", - "translated": "Kon geen goedkeuring aanvragen" - }, - { - "id": "native.apple.6580d3d1cdda682d", - "source": "Enable Talk", - "translated": "Talk inschakelen" - }, - { - "id": "native.apple.abbfc1ba85a230b4", - "source": "Sending a new pairing request to your gateway...", - "translated": "Een nieuw koppelingsverzoek naar je gateway verzenden..." - }, - { - "id": "native.apple.2880deb53ea2fd86", - "source": "Approve this request on your gateway. Talk will start automatically when approval lands.", - "translated": "Keur dit verzoek goed op je gateway. Talk start automatisch zodra de goedkeuring binnenkomt." - }, - { - "id": "native.apple.80faa829f543c03c", - "source": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider.", - "translated": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider." - }, - { - "id": "native.apple.88b0a2e2986fcebf", - "source": "Request Again", - "translated": "Opnieuw aanvragen" - }, - { - "id": "native.apple.5a3faae3472b4b86", - "source": "Send Approval Request", - "translated": "Goedkeuringsaanvraag verzenden" - }, { "id": "native.apple.1065cfe92d7c1eb6", "source": "Asking OpenClaw", @@ -22908,11 +22798,6 @@ "source": "Attachments", "translated": "Bijlagen" }, - { - "id": "native.apple.f8dda832ed76441d", - "source": "Voice note", - "translated": "Spraaknotitie" - }, { "id": "native.apple.ee2c0d22f5861159", "source": "Talk", @@ -23898,6 +23783,11 @@ "source": "Dismiss", "translated": "Sluiten" }, + { + "id": "native.apple.3892b7f55d9d1f0a", + "source": "Remove attachments or wait for delivery to resolve before starting a new chat.", + "translated": "Verwijder bijlagen of wacht tot de bezorging is voltooid voordat je een nieuwe chat start." + }, { "id": "native.apple.92fe9093903eda3b", "source": "Could not attach voice note: %@", @@ -23958,11 +23848,6 @@ "source": "Gateway changed before the session operation started.", "translated": "De Gateway is gewijzigd voordat de sessiebewerking is gestart." }, - { - "id": "native.apple.d93e6fe16ec1b0e5", - "source": "Remove attachments or wait for delivery to resolve before starting a new chat.", - "translated": "Verwijder bijlagen of wacht tot de bezorging is voltooid voordat je een nieuwe chat start." - }, { "id": "native.apple.710c7bd117a7cc12", "source": "Remove attachments or wait for delivery to resolve before switching chats.", @@ -24188,6 +24073,61 @@ "source": "Compact Session", "translated": "Sessie comprimeren" }, + { + "id": "native.apple.fa10740f9e272e83", + "source": "Voice note", + "translated": "Spraaknotitie" + }, + { + "id": "native.apple.e4de5afc37981d6c", + "source": "Photo Library", + "translated": "Fotobibliotheek" + }, + { + "id": "native.apple.37c19a66626b21fa", + "source": "Camera", + "translated": "Camera" + }, + { + "id": "native.apple.333682684163ba6b", + "source": "Choose Image File", + "translated": "Afbeeldingsbestand kiezen" + }, + { + "id": "native.apple.534246792690968d", + "source": "Add attachment", + "translated": "Bijlage toevoegen" + }, + { + "id": "native.apple.e33210feaa3da016", + "source": "Record Voice Note", + "translated": "Spraakbericht opnemen" + }, + { + "id": "native.apple.00f0e15858e38d19", + "source": "Dictate message", + "translated": "Bericht dicteren" + }, + { + "id": "native.apple.96f20175f61a2b29", + "source": "Listening", + "translated": "Luisteren" + }, + { + "id": "native.apple.eaa9047db6cfb67e", + "source": "Not listening", + "translated": "Luistert niet" + }, + { + "id": "native.apple.4185df7a940f09ef", + "source": "Finish dictation", + "translated": "Dicteren voltooien" + }, + { + "id": "native.apple.5a0793d2827b9399", + "source": "Transcribe speech into the message", + "translated": "Zet spraak om naar tekst in het bericht." + }, { "id": "native.apple.f74d150ce05205dd", "source": "z", diff --git a/apps/.i18n/native/pl.json b/apps/.i18n/native/pl.json index 0c164467c8d4..aa712e5578cb 100644 --- a/apps/.i18n/native/pl.json +++ b/apps/.i18n/native/pl.json @@ -8358,11 +8358,21 @@ "source": "OpenClaw Activity", "translated": "Aktywność OpenClaw" }, + { + "id": "native.apple.093081590556db7f", + "source": "OPENCLAW", + "translated": "OPENCLAW" + }, { "id": "native.apple.86d546e1f1247028", "source": "OpenClaw", "translated": "OpenClaw" }, + { + "id": "native.apple.9fd01bdb5a753f3e", + "source": "LIVE", + "translated": "LIVE" + }, { "id": "native.apple.417ac9273b5f63ca", "source": "Connecting...", @@ -8383,6 +8393,36 @@ "source": "Action required", "translated": "Wymagane działanie" }, + { + "id": "native.apple.275026f681242715", + "source": "Using %@", + "translated": "Przez %@" + }, + { + "id": "native.apple.360d0d0fc0df9eb2", + "source": "Using a tool", + "translated": "Korzystanie z narzędzia" + }, + { + "id": "native.apple.0d0ee660962ea840", + "source": "Listening", + "translated": "Słuchanie" + }, + { + "id": "native.apple.095ebf1496feb2ad", + "source": "Speaking", + "translated": "Mówienie" + }, + { + "id": "native.apple.63c98d2ca6a5b09c", + "source": "Voice active", + "translated": "Głos włączony" + }, + { + "id": "native.apple.02470f4b5348f97c", + "source": "Paused", + "translated": "Wstrzymano" + }, { "id": "native.apple.f2f3e6437fafcd1c", "source": "Connected", @@ -10518,11 +10558,36 @@ "source": "The session attaches once the gateway is ready.", "translated": "Sesja zostanie dołączona, gdy Gateway będzie gotowy." }, + { + "id": "native.apple.02611ddddef7295a", + "source": "Collapsed", + "translated": "Zwinięte" + }, + { + "id": "native.apple.1f4911f4a05230b5", + "source": "Expanded", + "translated": "Rozwinięte" + }, { "id": "native.apple.d19c9577e0fdfea7", "source": "Opens Settings / Gateway", "translated": "Otwiera Ustawienia / Gateway" }, + { + "id": "native.apple.b59e690104ae3e24", + "source": "Hides the gateway status label", + "translated": "Ukrywa etykietę stanu bramy" + }, + { + "id": "native.apple.5fcf9d462463dd27", + "source": "Shows the full gateway status label", + "translated": "Pokazuje pełną etykietę stanu bramy" + }, + { + "id": "native.apple.592b1e673e42df50", + "source": "Voice off", + "translated": "Głos wyłączony" + }, { "id": "native.apple.7c87384c7d1370e1", "source": "New Chat", @@ -10548,6 +10613,11 @@ "source": "Export Transcript", "translated": "Eksportuj transkrypcję" }, + { + "id": "native.apple.d510e012c91b24ed", + "source": "Gateway Settings", + "translated": "Ustawienia bramy" + }, { "id": "native.apple.f26cef22f417abb4", "source": "Show reasoning & tool activity", @@ -11728,11 +11798,6 @@ "source": "Agent chat and recent work.", "translated": "Czat agenta i ostatnia praca." }, - { - "id": "native.apple.a30d0a950d0a77aa", - "source": "Realtime voice and controls.", - "translated": "Głos i sterowanie w czasie rzeczywistym." - }, { "id": "native.apple.a03e961ab14ba060", "source": "Overview", @@ -13818,151 +13883,6 @@ "source": "ClawHub did not report an installable version for this skill.", "translated": "ClawHub nie zgłosił wersji tego skillu dostępnej do instalacji." }, - { - "id": "native.apple.798c3584ad95f8da", - "source": "Enable Talk", - "translated": "Włącz Talk" - }, - { - "id": "native.apple.c6e171128190d131", - "source": "Not Now", - "translated": "Nie teraz" - }, - { - "id": "native.apple.e4b15aef3c2e3da7", - "source": "Talk", - "translated": "Rozmowa" - }, - { - "id": "native.apple.ab3381742bdff650", - "source": "Conversation", - "translated": "Konwersacja" - }, - { - "id": "native.apple.7af100f6640c6a69", - "source": "Agent", - "translated": "Agent" - }, - { - "id": "native.apple.49c4de60117b1a98", - "source": "Session", - "translated": "Sesja" - }, - { - "id": "native.apple.37c29d2506b955e9", - "source": "Runtime", - "translated": "Środowisko uruchomieniowe" - }, - { - "id": "native.apple.13e0ad79b141576e", - "source": "Voice Mode", - "translated": "Tryb głosowy" - }, - { - "id": "native.apple.e8035b71bab833c7", - "source": "Configured", - "translated": "Skonfigurowano" - }, - { - "id": "native.apple.1b2845ec89d20eef", - "source": "Active", - "translated": "Aktywne" - }, - { - "id": "native.apple.a82c07df64d13b23", - "source": "Transport", - "translated": "Transport" - }, - { - "id": "native.apple.384e097278f76954", - "source": "Last issue", - "translated": "Ostatni problem" - }, - { - "id": "native.apple.a6d8fc7bfd98836f", - "source": "Permission", - "translated": "Uprawnienie" - }, - { - "id": "native.apple.983bdcd5e3f1baa8", - "source": "Speech language", - "translated": "Język mowy" - }, - { - "id": "native.apple.32e7fa2b1798e9c3", - "source": "Controls", - "translated": "Elementy sterujące" - }, - { - "id": "native.apple.0eb012a5d3d81b32", - "source": "Speakerphone", - "translated": "Tryb głośnomówiący" - }, - { - "id": "native.apple.ef8699e2718346da", - "source": "Background listening", - "translated": "Nasłuchiwanie w tle" - }, - { - "id": "native.apple.1e6938a09b4fbe77", - "source": "Voice & Talk Settings", - "translated": "Ustawienia głosu i rozmowy" - }, - { - "id": "native.apple.c7a591fd5bb6687e", - "source": "Gateway approval is required before this phone can capture voice.", - "translated": "Zanim ten telefon będzie mógł przechwytywać głos, wymagane jest zatwierdzenie przez Gateway." - }, - { - "id": "native.apple.9d40e75e55ed39eb", - "source": "Voice is disabled in Apple Review demo mode.", - "translated": "Obsługa głosu jest wyłączona w trybie demonstracyjnym Apple Review." - }, - { - "id": "native.apple.3ef69bf916177799", - "source": "Connect to your gateway to start a voice conversation.", - "translated": "Połącz się z Gateway, aby rozpocząć rozmowę głosową." - }, - { - "id": "native.apple.ed03af7c68841a5d", - "source": "Open Voice settings after the gateway loads Talk configuration.", - "translated": "Otwórz ustawienia głosu po wczytaniu konfiguracji Talk przez Gateway." - }, - { - "id": "native.apple.883a251978601b96", - "source": "Routes voice to %@.", - "translated": "Kieruje głos do %@." - }, - { - "id": "native.apple.877fb5c1abfaafa1", - "source": "Not loaded", - "translated": "Not loaded" - }, - { - "id": "native.apple.d3ccb092936551fc", - "source": "Not active", - "translated": "Nieaktywny" - }, - { - "id": "native.apple.1620f7cb3deea133", - "source": "Open Gateway Settings", - "translated": "Otwórz ustawienia Gateway" - }, - { - "id": "native.apple.3fd2706ef0f14fd2", - "source": "Open Voice Settings", - "translated": "Otwórz ustawienia głosu" - }, - { - "id": "native.apple.3cc9cd23a67db077", - "source": "Demo Mode Only", - "translated": "Tylko tryb demonstracyjny" - }, - { - "id": "native.apple.12c9d8231777366c", - "source": "Waiting for Approval", - "translated": "Oczekiwanie na zatwierdzenie" - }, { "id": "native.apple.dc57861a3ac5a08e", "source": "Open Settings", @@ -14528,6 +14448,41 @@ "source": "Action required", "translated": "Wymagane działanie" }, + { + "id": "native.apple.7205f0822706cb55", + "source": "Listening", + "translated": "Słuchanie" + }, + { + "id": "native.apple.8b10cb1dc21e42af", + "source": "Listening (PTT)", + "translated": "Nasłuchiwanie (PTT)" + }, + { + "id": "native.apple.939c7ec55636c67f", + "source": "Listening (Realtime)", + "translated": "Nasłuchiwanie (w czasie rzeczywistym)" + }, + { + "id": "native.apple.86f203ebb2453d15", + "source": "Speaking…", + "translated": "Mówienie…" + }, + { + "id": "native.apple.1e3ff6b56ae3828d", + "source": "Speaking (System)…", + "translated": "Mówienie (system)…" + }, + { + "id": "native.apple.37a5fedaefb8ebf3", + "source": "Generating voice…", + "translated": "Generowanie głosu…" + }, + { + "id": "native.apple.47003f094346c660", + "source": "Ready", + "translated": "Gotowe" + }, { "id": "native.apple.cf0450b542893fb5", "source": "Off", @@ -15348,26 +15303,6 @@ "source": "unknown relay error", "translated": "unknown relay error" }, - { - "id": "native.apple.f2ea327bfea8b8e3", - "source": "Talk", - "translated": "Rozmawiaj" - }, - { - "id": "native.apple.bda6cfa988e49a75", - "source": "Control", - "translated": "Sterowanie" - }, - { - "id": "native.apple.3c80f5351055a5f3", - "source": "Agent", - "translated": "Agent" - }, - { - "id": "native.apple.22d107db5b905ef4", - "source": "Settings", - "translated": "Ustawienia" - }, { "id": "native.apple.c23d35de8d70863f", "source": "OpenClaw", @@ -15458,6 +15393,26 @@ "source": "Close canvas", "translated": "Zamknij obszar roboczy" }, + { + "id": "native.apple.a1db6ce0aed1d18e", + "source": "Chat, voice active", + "translated": "Czat, głos włączony" + }, + { + "id": "native.apple.bda6cfa988e49a75", + "source": "Control", + "translated": "Sterowanie" + }, + { + "id": "native.apple.3c80f5351055a5f3", + "source": "Agent", + "translated": "Agent" + }, + { + "id": "native.apple.22d107db5b905ef4", + "source": "Settings", + "translated": "Ustawienia" + }, { "id": "native.apple.8a4751c86aa6fe77", "source": "Gateway needs attention", @@ -15488,11 +15443,6 @@ "source": "Chat", "translated": "Czat" }, - { - "id": "native.apple.febca7528af94342", - "source": "Talk", - "translated": "Rozmowa" - }, { "id": "native.apple.e26bbe303d8a9544", "source": "Overview", @@ -15863,6 +15813,11 @@ "source": "Requesting permissions…", "translated": "Prośba o uprawnienia…" }, + { + "id": "native.apple.198f59c97d7afe11", + "source": "Start failed: %@", + "translated": "Uruchomienie nie powiodło się: %@" + }, { "id": "native.apple.a1910b5c2e008b6c", "source": "Microphone permission denied", @@ -15873,11 +15828,6 @@ "source": "Speech recognition", "translated": "Rozpoznawanie mowy" }, - { - "id": "native.apple.198f59c97d7afe11", - "source": "Start failed: %@", - "translated": "Uruchomienie nie powiodło się: %@" - }, { "id": "native.apple.c09f4e3bbef8177a", "source": "Offline", @@ -16088,66 +16038,6 @@ "source": "Listening (PTT)", "translated": "Nasłuchiwanie (PTT)" }, - { - "id": "native.apple.0189269b6a863dc2", - "source": "Request ID", - "translated": "ID żądania" - }, - { - "id": "native.apple.f9d69f27f9d7f4de", - "source": "Sending...", - "translated": "Wysyłanie..." - }, - { - "id": "native.apple.0c5336129f70faed", - "source": "Retry", - "translated": "Ponów" - }, - { - "id": "native.apple.7b23f0a89d3ad411", - "source": "Sending approval request", - "translated": "Wysyłanie prośby o zatwierdzenie" - }, - { - "id": "native.apple.55db7317a2542e51", - "source": "Approval sent", - "translated": "Zatwierdzenie wysłane" - }, - { - "id": "native.apple.06330f62ab254a0e", - "source": "Could not request approval", - "translated": "Nie udało się poprosić o zatwierdzenie" - }, - { - "id": "native.apple.6580d3d1cdda682d", - "source": "Enable Talk", - "translated": "Włącz Talk" - }, - { - "id": "native.apple.abbfc1ba85a230b4", - "source": "Sending a new pairing request to your gateway...", - "translated": "Wysyłanie nowej prośby o parowanie do Twojego gateway..." - }, - { - "id": "native.apple.2880deb53ea2fd86", - "source": "Approve this request on your gateway. Talk will start automatically when approval lands.", - "translated": "Zatwierdź tę prośbę na swoim gateway. Talk uruchomi się automatycznie po otrzymaniu zatwierdzenia." - }, - { - "id": "native.apple.80faa829f543c03c", - "source": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider.", - "translated": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider." - }, - { - "id": "native.apple.88b0a2e2986fcebf", - "source": "Request Again", - "translated": "Poproś ponownie" - }, - { - "id": "native.apple.5a3faae3472b4b86", - "source": "Send Approval Request", - "translated": "Wyślij prośbę o zatwierdzenie" - }, { "id": "native.apple.1065cfe92d7c1eb6", "source": "Asking OpenClaw", @@ -22908,11 +22798,6 @@ "source": "Attachments", "translated": "Załączniki" }, - { - "id": "native.apple.f8dda832ed76441d", - "source": "Voice note", - "translated": "Notatka głosowa" - }, { "id": "native.apple.ee2c0d22f5861159", "source": "Talk", @@ -23898,6 +23783,11 @@ "source": "Dismiss", "translated": "Odrzuć" }, + { + "id": "native.apple.3892b7f55d9d1f0a", + "source": "Remove attachments or wait for delivery to resolve before starting a new chat.", + "translated": "Usuń załączniki lub poczekaj na zakończenie dostarczania przed rozpoczęciem nowego czatu." + }, { "id": "native.apple.92fe9093903eda3b", "source": "Could not attach voice note: %@", @@ -23958,11 +23848,6 @@ "source": "Gateway changed before the session operation started.", "translated": "Gateway zmienił się przed rozpoczęciem operacji na sesji." }, - { - "id": "native.apple.d93e6fe16ec1b0e5", - "source": "Remove attachments or wait for delivery to resolve before starting a new chat.", - "translated": "Usuń załączniki lub poczekaj na zakończenie dostarczania przed rozpoczęciem nowego czatu." - }, { "id": "native.apple.710c7bd117a7cc12", "source": "Remove attachments or wait for delivery to resolve before switching chats.", @@ -24188,6 +24073,61 @@ "source": "Compact Session", "translated": "Kompaktuj sesję" }, + { + "id": "native.apple.fa10740f9e272e83", + "source": "Voice note", + "translated": "Notatka głosowa" + }, + { + "id": "native.apple.e4de5afc37981d6c", + "source": "Photo Library", + "translated": "Biblioteka zdjęć" + }, + { + "id": "native.apple.37c19a66626b21fa", + "source": "Camera", + "translated": "Aparat" + }, + { + "id": "native.apple.333682684163ba6b", + "source": "Choose Image File", + "translated": "Wybierz plik obrazu" + }, + { + "id": "native.apple.534246792690968d", + "source": "Add attachment", + "translated": "Dodaj załącznik" + }, + { + "id": "native.apple.e33210feaa3da016", + "source": "Record Voice Note", + "translated": "Nagraj notatkę głosową" + }, + { + "id": "native.apple.00f0e15858e38d19", + "source": "Dictate message", + "translated": "Dyktuj wiadomość" + }, + { + "id": "native.apple.96f20175f61a2b29", + "source": "Listening", + "translated": "Słuchanie" + }, + { + "id": "native.apple.eaa9047db6cfb67e", + "source": "Not listening", + "translated": "Nie nasłuchuje" + }, + { + "id": "native.apple.4185df7a940f09ef", + "source": "Finish dictation", + "translated": "Zakończ dyktowanie" + }, + { + "id": "native.apple.5a0793d2827b9399", + "source": "Transcribe speech into the message", + "translated": "Przepisz mowę do wiadomości." + }, { "id": "native.apple.f74d150ce05205dd", "source": "z", diff --git a/apps/.i18n/native/pt-BR.json b/apps/.i18n/native/pt-BR.json index fe8b1ee3d896..fa6bf768a3d4 100644 --- a/apps/.i18n/native/pt-BR.json +++ b/apps/.i18n/native/pt-BR.json @@ -8358,11 +8358,21 @@ "source": "OpenClaw Activity", "translated": "Atividade do OpenClaw" }, + { + "id": "native.apple.093081590556db7f", + "source": "OPENCLAW", + "translated": "OPENCLAW" + }, { "id": "native.apple.86d546e1f1247028", "source": "OpenClaw", "translated": "OpenClaw" }, + { + "id": "native.apple.9fd01bdb5a753f3e", + "source": "LIVE", + "translated": "LIVE" + }, { "id": "native.apple.417ac9273b5f63ca", "source": "Connecting...", @@ -8383,6 +8393,36 @@ "source": "Action required", "translated": "Ação necessária" }, + { + "id": "native.apple.275026f681242715", + "source": "Using %@", + "translated": "Usando %@" + }, + { + "id": "native.apple.360d0d0fc0df9eb2", + "source": "Using a tool", + "translated": "Usando uma ferramenta" + }, + { + "id": "native.apple.0d0ee660962ea840", + "source": "Listening", + "translated": "Ouvindo" + }, + { + "id": "native.apple.095ebf1496feb2ad", + "source": "Speaking", + "translated": "Falando" + }, + { + "id": "native.apple.63c98d2ca6a5b09c", + "source": "Voice active", + "translated": "Voz ativa" + }, + { + "id": "native.apple.02470f4b5348f97c", + "source": "Paused", + "translated": "Pausado" + }, { "id": "native.apple.f2f3e6437fafcd1c", "source": "Connected", @@ -10518,11 +10558,36 @@ "source": "The session attaches once the gateway is ready.", "translated": "A sessão será anexada assim que o Gateway estiver pronto." }, + { + "id": "native.apple.02611ddddef7295a", + "source": "Collapsed", + "translated": "Recolhido" + }, + { + "id": "native.apple.1f4911f4a05230b5", + "source": "Expanded", + "translated": "Expandido" + }, { "id": "native.apple.d19c9577e0fdfea7", "source": "Opens Settings / Gateway", "translated": "Abre Ajustes / Gateway" }, + { + "id": "native.apple.b59e690104ae3e24", + "source": "Hides the gateway status label", + "translated": "Oculta o rótulo de status do gateway" + }, + { + "id": "native.apple.5fcf9d462463dd27", + "source": "Shows the full gateway status label", + "translated": "Mostra o rótulo completo de status do gateway" + }, + { + "id": "native.apple.592b1e673e42df50", + "source": "Voice off", + "translated": "Voz desativada" + }, { "id": "native.apple.7c87384c7d1370e1", "source": "New Chat", @@ -10548,6 +10613,11 @@ "source": "Export Transcript", "translated": "Exportar transcrição" }, + { + "id": "native.apple.d510e012c91b24ed", + "source": "Gateway Settings", + "translated": "Ajustes do gateway" + }, { "id": "native.apple.f26cef22f417abb4", "source": "Show reasoning & tool activity", @@ -11728,11 +11798,6 @@ "source": "Agent chat and recent work.", "translated": "Chat do agente e trabalho recente." }, - { - "id": "native.apple.a30d0a950d0a77aa", - "source": "Realtime voice and controls.", - "translated": "Voz e controles em tempo real." - }, { "id": "native.apple.a03e961ab14ba060", "source": "Overview", @@ -13818,151 +13883,6 @@ "source": "ClawHub did not report an installable version for this skill.", "translated": "O ClawHub não informou uma versão instalável para esta skill." }, - { - "id": "native.apple.798c3584ad95f8da", - "source": "Enable Talk", - "translated": "Ativar conversa" - }, - { - "id": "native.apple.c6e171128190d131", - "source": "Not Now", - "translated": "Agora não" - }, - { - "id": "native.apple.e4b15aef3c2e3da7", - "source": "Talk", - "translated": "Falar" - }, - { - "id": "native.apple.ab3381742bdff650", - "source": "Conversation", - "translated": "Conversa" - }, - { - "id": "native.apple.7af100f6640c6a69", - "source": "Agent", - "translated": "Agente" - }, - { - "id": "native.apple.49c4de60117b1a98", - "source": "Session", - "translated": "Sessão" - }, - { - "id": "native.apple.37c29d2506b955e9", - "source": "Runtime", - "translated": "Tempo de execução" - }, - { - "id": "native.apple.13e0ad79b141576e", - "source": "Voice Mode", - "translated": "Modo de voz" - }, - { - "id": "native.apple.e8035b71bab833c7", - "source": "Configured", - "translated": "Configurado" - }, - { - "id": "native.apple.1b2845ec89d20eef", - "source": "Active", - "translated": "Ativo" - }, - { - "id": "native.apple.a82c07df64d13b23", - "source": "Transport", - "translated": "Transporte" - }, - { - "id": "native.apple.384e097278f76954", - "source": "Last issue", - "translated": "Último problema" - }, - { - "id": "native.apple.a6d8fc7bfd98836f", - "source": "Permission", - "translated": "Permissão" - }, - { - "id": "native.apple.983bdcd5e3f1baa8", - "source": "Speech language", - "translated": "Idioma da fala" - }, - { - "id": "native.apple.32e7fa2b1798e9c3", - "source": "Controls", - "translated": "Controles" - }, - { - "id": "native.apple.0eb012a5d3d81b32", - "source": "Speakerphone", - "translated": "Viva-voz" - }, - { - "id": "native.apple.ef8699e2718346da", - "source": "Background listening", - "translated": "Escuta em segundo plano" - }, - { - "id": "native.apple.1e6938a09b4fbe77", - "source": "Voice & Talk Settings", - "translated": "Configurações de voz e fala" - }, - { - "id": "native.apple.c7a591fd5bb6687e", - "source": "Gateway approval is required before this phone can capture voice.", - "translated": "A aprovação do Gateway é necessária para que este telefone possa capturar voz." - }, - { - "id": "native.apple.9d40e75e55ed39eb", - "source": "Voice is disabled in Apple Review demo mode.", - "translated": "A voz está desativada no modo de demonstração para a revisão da Apple." - }, - { - "id": "native.apple.3ef69bf916177799", - "source": "Connect to your gateway to start a voice conversation.", - "translated": "Conecte-se ao seu Gateway para iniciar uma conversa por voz." - }, - { - "id": "native.apple.ed03af7c68841a5d", - "source": "Open Voice settings after the gateway loads Talk configuration.", - "translated": "Abra os ajustes de Voz depois que o Gateway carregar a configuração do Talk." - }, - { - "id": "native.apple.883a251978601b96", - "source": "Routes voice to %@.", - "translated": "Encaminha a voz para %@." - }, - { - "id": "native.apple.877fb5c1abfaafa1", - "source": "Not loaded", - "translated": "Not loaded" - }, - { - "id": "native.apple.d3ccb092936551fc", - "source": "Not active", - "translated": "Inativo" - }, - { - "id": "native.apple.1620f7cb3deea133", - "source": "Open Gateway Settings", - "translated": "Abrir configurações do Gateway" - }, - { - "id": "native.apple.3fd2706ef0f14fd2", - "source": "Open Voice Settings", - "translated": "Abrir configurações de voz" - }, - { - "id": "native.apple.3cc9cd23a67db077", - "source": "Demo Mode Only", - "translated": "Apenas modo de demonstração" - }, - { - "id": "native.apple.12c9d8231777366c", - "source": "Waiting for Approval", - "translated": "Aguardando aprovação" - }, { "id": "native.apple.dc57861a3ac5a08e", "source": "Open Settings", @@ -14528,6 +14448,41 @@ "source": "Action required", "translated": "Ação necessária" }, + { + "id": "native.apple.7205f0822706cb55", + "source": "Listening", + "translated": "Ouvindo" + }, + { + "id": "native.apple.8b10cb1dc21e42af", + "source": "Listening (PTT)", + "translated": "Ouvindo (PTT)" + }, + { + "id": "native.apple.939c7ec55636c67f", + "source": "Listening (Realtime)", + "translated": "Ouvindo (em tempo real)" + }, + { + "id": "native.apple.86f203ebb2453d15", + "source": "Speaking…", + "translated": "Falando…" + }, + { + "id": "native.apple.1e3ff6b56ae3828d", + "source": "Speaking (System)…", + "translated": "Falando (Sistema)…" + }, + { + "id": "native.apple.37a5fedaefb8ebf3", + "source": "Generating voice…", + "translated": "Gerando voz…" + }, + { + "id": "native.apple.47003f094346c660", + "source": "Ready", + "translated": "Pronto" + }, { "id": "native.apple.cf0450b542893fb5", "source": "Off", @@ -15348,26 +15303,6 @@ "source": "unknown relay error", "translated": "unknown relay error" }, - { - "id": "native.apple.f2ea327bfea8b8e3", - "source": "Talk", - "translated": "Conversar" - }, - { - "id": "native.apple.bda6cfa988e49a75", - "source": "Control", - "translated": "Controle" - }, - { - "id": "native.apple.3c80f5351055a5f3", - "source": "Agent", - "translated": "Agente" - }, - { - "id": "native.apple.22d107db5b905ef4", - "source": "Settings", - "translated": "Configurações" - }, { "id": "native.apple.c23d35de8d70863f", "source": "OpenClaw", @@ -15458,6 +15393,26 @@ "source": "Close canvas", "translated": "Fechar tela" }, + { + "id": "native.apple.a1db6ce0aed1d18e", + "source": "Chat, voice active", + "translated": "Chat, voz ativa" + }, + { + "id": "native.apple.bda6cfa988e49a75", + "source": "Control", + "translated": "Controle" + }, + { + "id": "native.apple.3c80f5351055a5f3", + "source": "Agent", + "translated": "Agente" + }, + { + "id": "native.apple.22d107db5b905ef4", + "source": "Settings", + "translated": "Configurações" + }, { "id": "native.apple.8a4751c86aa6fe77", "source": "Gateway needs attention", @@ -15488,11 +15443,6 @@ "source": "Chat", "translated": "Chat" }, - { - "id": "native.apple.febca7528af94342", - "source": "Talk", - "translated": "Falar" - }, { "id": "native.apple.e26bbe303d8a9544", "source": "Overview", @@ -15863,6 +15813,11 @@ "source": "Requesting permissions…", "translated": "Solicitando permissões…" }, + { + "id": "native.apple.198f59c97d7afe11", + "source": "Start failed: %@", + "translated": "Falha ao iniciar: %@" + }, { "id": "native.apple.a1910b5c2e008b6c", "source": "Microphone permission denied", @@ -15873,11 +15828,6 @@ "source": "Speech recognition", "translated": "Reconhecimento de fala" }, - { - "id": "native.apple.198f59c97d7afe11", - "source": "Start failed: %@", - "translated": "Falha ao iniciar: %@" - }, { "id": "native.apple.c09f4e3bbef8177a", "source": "Offline", @@ -16088,66 +16038,6 @@ "source": "Listening (PTT)", "translated": "Ouvindo (PTT)" }, - { - "id": "native.apple.0189269b6a863dc2", - "source": "Request ID", - "translated": "ID da solicitação" - }, - { - "id": "native.apple.f9d69f27f9d7f4de", - "source": "Sending...", - "translated": "Enviando..." - }, - { - "id": "native.apple.0c5336129f70faed", - "source": "Retry", - "translated": "Tentar novamente" - }, - { - "id": "native.apple.7b23f0a89d3ad411", - "source": "Sending approval request", - "translated": "Enviando solicitação de aprovação" - }, - { - "id": "native.apple.55db7317a2542e51", - "source": "Approval sent", - "translated": "Aprovação enviada" - }, - { - "id": "native.apple.06330f62ab254a0e", - "source": "Could not request approval", - "translated": "Não foi possível solicitar aprovação" - }, - { - "id": "native.apple.6580d3d1cdda682d", - "source": "Enable Talk", - "translated": "Ativar Talk" - }, - { - "id": "native.apple.abbfc1ba85a230b4", - "source": "Sending a new pairing request to your gateway...", - "translated": "Enviando uma nova solicitação de pareamento para seu gateway..." - }, - { - "id": "native.apple.2880deb53ea2fd86", - "source": "Approve this request on your gateway. Talk will start automatically when approval lands.", - "translated": "Aprove esta solicitação no seu gateway. O Talk será iniciado automaticamente quando a aprovação chegar." - }, - { - "id": "native.apple.80faa829f543c03c", - "source": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider.", - "translated": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider." - }, - { - "id": "native.apple.88b0a2e2986fcebf", - "source": "Request Again", - "translated": "Solicitar novamente" - }, - { - "id": "native.apple.5a3faae3472b4b86", - "source": "Send Approval Request", - "translated": "Enviar solicitação de aprovação" - }, { "id": "native.apple.1065cfe92d7c1eb6", "source": "Asking OpenClaw", @@ -22908,11 +22798,6 @@ "source": "Attachments", "translated": "Anexos" }, - { - "id": "native.apple.f8dda832ed76441d", - "source": "Voice note", - "translated": "Nota de voz" - }, { "id": "native.apple.ee2c0d22f5861159", "source": "Talk", @@ -23898,6 +23783,11 @@ "source": "Dismiss", "translated": "Dispensar" }, + { + "id": "native.apple.3892b7f55d9d1f0a", + "source": "Remove attachments or wait for delivery to resolve before starting a new chat.", + "translated": "Remova os anexos ou aguarde a entrega ser concluída antes de iniciar uma nova conversa." + }, { "id": "native.apple.92fe9093903eda3b", "source": "Could not attach voice note: %@", @@ -23958,11 +23848,6 @@ "source": "Gateway changed before the session operation started.", "translated": "O Gateway foi alterado antes do início da operação da sessão." }, - { - "id": "native.apple.d93e6fe16ec1b0e5", - "source": "Remove attachments or wait for delivery to resolve before starting a new chat.", - "translated": "Remova os anexos ou aguarde a entrega ser concluída antes de iniciar uma nova conversa." - }, { "id": "native.apple.710c7bd117a7cc12", "source": "Remove attachments or wait for delivery to resolve before switching chats.", @@ -24188,6 +24073,61 @@ "source": "Compact Session", "translated": "Compactar sessão" }, + { + "id": "native.apple.fa10740f9e272e83", + "source": "Voice note", + "translated": "Nota de voz" + }, + { + "id": "native.apple.e4de5afc37981d6c", + "source": "Photo Library", + "translated": "Fototeca" + }, + { + "id": "native.apple.37c19a66626b21fa", + "source": "Camera", + "translated": "Câmera" + }, + { + "id": "native.apple.333682684163ba6b", + "source": "Choose Image File", + "translated": "Escolher arquivo de imagem" + }, + { + "id": "native.apple.534246792690968d", + "source": "Add attachment", + "translated": "Adicionar anexo" + }, + { + "id": "native.apple.e33210feaa3da016", + "source": "Record Voice Note", + "translated": "Gravar nota de voz" + }, + { + "id": "native.apple.00f0e15858e38d19", + "source": "Dictate message", + "translated": "Ditar mensagem" + }, + { + "id": "native.apple.96f20175f61a2b29", + "source": "Listening", + "translated": "Ouvindo" + }, + { + "id": "native.apple.eaa9047db6cfb67e", + "source": "Not listening", + "translated": "Não está ouvindo" + }, + { + "id": "native.apple.4185df7a940f09ef", + "source": "Finish dictation", + "translated": "Encerrar ditado" + }, + { + "id": "native.apple.5a0793d2827b9399", + "source": "Transcribe speech into the message", + "translated": "Transcreva a fala na mensagem." + }, { "id": "native.apple.f74d150ce05205dd", "source": "z", diff --git a/apps/.i18n/native/ru.json b/apps/.i18n/native/ru.json index f82e8dcb9caa..d3fc2984577f 100644 --- a/apps/.i18n/native/ru.json +++ b/apps/.i18n/native/ru.json @@ -8358,11 +8358,21 @@ "source": "OpenClaw Activity", "translated": "Активность OpenClaw" }, + { + "id": "native.apple.093081590556db7f", + "source": "OPENCLAW", + "translated": "OPENCLAW" + }, { "id": "native.apple.86d546e1f1247028", "source": "OpenClaw", "translated": "OpenClaw" }, + { + "id": "native.apple.9fd01bdb5a753f3e", + "source": "LIVE", + "translated": "LIVE" + }, { "id": "native.apple.417ac9273b5f63ca", "source": "Connecting...", @@ -8383,6 +8393,36 @@ "source": "Action required", "translated": "Требуется действие" }, + { + "id": "native.apple.275026f681242715", + "source": "Using %@", + "translated": "С использованием %@" + }, + { + "id": "native.apple.360d0d0fc0df9eb2", + "source": "Using a tool", + "translated": "С помощью инструмента" + }, + { + "id": "native.apple.0d0ee660962ea840", + "source": "Listening", + "translated": "Прослушивание" + }, + { + "id": "native.apple.095ebf1496feb2ad", + "source": "Speaking", + "translated": "Воспроизведение речи" + }, + { + "id": "native.apple.63c98d2ca6a5b09c", + "source": "Voice active", + "translated": "Голос включён" + }, + { + "id": "native.apple.02470f4b5348f97c", + "source": "Paused", + "translated": "Приостановлено" + }, { "id": "native.apple.f2f3e6437fafcd1c", "source": "Connected", @@ -10518,11 +10558,36 @@ "source": "The session attaches once the gateway is ready.", "translated": "Сеанс подключится, когда Gateway будет готов." }, + { + "id": "native.apple.02611ddddef7295a", + "source": "Collapsed", + "translated": "Свернуто" + }, + { + "id": "native.apple.1f4911f4a05230b5", + "source": "Expanded", + "translated": "Развернуто" + }, { "id": "native.apple.d19c9577e0fdfea7", "source": "Opens Settings / Gateway", "translated": "Открывает Settings / Gateway" }, + { + "id": "native.apple.b59e690104ae3e24", + "source": "Hides the gateway status label", + "translated": "Скрывает метку состояния шлюза" + }, + { + "id": "native.apple.5fcf9d462463dd27", + "source": "Shows the full gateway status label", + "translated": "Показывает полную метку состояния шлюза" + }, + { + "id": "native.apple.592b1e673e42df50", + "source": "Voice off", + "translated": "Голос выключен" + }, { "id": "native.apple.7c87384c7d1370e1", "source": "New Chat", @@ -10548,6 +10613,11 @@ "source": "Export Transcript", "translated": "Экспортировать стенограмму" }, + { + "id": "native.apple.d510e012c91b24ed", + "source": "Gateway Settings", + "translated": "Настройки шлюза" + }, { "id": "native.apple.f26cef22f417abb4", "source": "Show reasoning & tool activity", @@ -11728,11 +11798,6 @@ "source": "Agent chat and recent work.", "translated": "Чат с агентом и недавняя работа." }, - { - "id": "native.apple.a30d0a950d0a77aa", - "source": "Realtime voice and controls.", - "translated": "Голос и управление в реальном времени." - }, { "id": "native.apple.a03e961ab14ba060", "source": "Overview", @@ -13818,151 +13883,6 @@ "source": "ClawHub did not report an installable version for this skill.", "translated": "ClawHub не сообщил об устанавливаемой версии этого навыка." }, - { - "id": "native.apple.798c3584ad95f8da", - "source": "Enable Talk", - "translated": "Включить разговор" - }, - { - "id": "native.apple.c6e171128190d131", - "source": "Not Now", - "translated": "Не сейчас" - }, - { - "id": "native.apple.e4b15aef3c2e3da7", - "source": "Talk", - "translated": "Разговор" - }, - { - "id": "native.apple.ab3381742bdff650", - "source": "Conversation", - "translated": "Беседа" - }, - { - "id": "native.apple.7af100f6640c6a69", - "source": "Agent", - "translated": "Агент" - }, - { - "id": "native.apple.49c4de60117b1a98", - "source": "Session", - "translated": "Сеанс" - }, - { - "id": "native.apple.37c29d2506b955e9", - "source": "Runtime", - "translated": "Среда выполнения" - }, - { - "id": "native.apple.13e0ad79b141576e", - "source": "Voice Mode", - "translated": "Голосовой режим" - }, - { - "id": "native.apple.e8035b71bab833c7", - "source": "Configured", - "translated": "Настроено" - }, - { - "id": "native.apple.1b2845ec89d20eef", - "source": "Active", - "translated": "Активно" - }, - { - "id": "native.apple.a82c07df64d13b23", - "source": "Transport", - "translated": "Транспорт" - }, - { - "id": "native.apple.384e097278f76954", - "source": "Last issue", - "translated": "Последняя проблема" - }, - { - "id": "native.apple.a6d8fc7bfd98836f", - "source": "Permission", - "translated": "Разрешение" - }, - { - "id": "native.apple.983bdcd5e3f1baa8", - "source": "Speech language", - "translated": "Язык речи" - }, - { - "id": "native.apple.32e7fa2b1798e9c3", - "source": "Controls", - "translated": "Элементы управления" - }, - { - "id": "native.apple.0eb012a5d3d81b32", - "source": "Speakerphone", - "translated": "Громкая связь" - }, - { - "id": "native.apple.ef8699e2718346da", - "source": "Background listening", - "translated": "Фоновое прослушивание" - }, - { - "id": "native.apple.1e6938a09b4fbe77", - "source": "Voice & Talk Settings", - "translated": "Настройки голоса и разговора" - }, - { - "id": "native.apple.c7a591fd5bb6687e", - "source": "Gateway approval is required before this phone can capture voice.", - "translated": "Прежде чем этот телефон сможет записывать голос, требуется одобрение Gateway." - }, - { - "id": "native.apple.9d40e75e55ed39eb", - "source": "Voice is disabled in Apple Review demo mode.", - "translated": "Голосовые функции отключены в демонстрационном режиме Apple Review." - }, - { - "id": "native.apple.3ef69bf916177799", - "source": "Connect to your gateway to start a voice conversation.", - "translated": "Подключитесь к Gateway, чтобы начать голосовой разговор." - }, - { - "id": "native.apple.ed03af7c68841a5d", - "source": "Open Voice settings after the gateway loads Talk configuration.", - "translated": "Откройте настройки голоса после того, как Gateway загрузит конфигурацию Talk." - }, - { - "id": "native.apple.883a251978601b96", - "source": "Routes voice to %@.", - "translated": "Направляет голосовой ввод в %@." - }, - { - "id": "native.apple.877fb5c1abfaafa1", - "source": "Not loaded", - "translated": "Not loaded" - }, - { - "id": "native.apple.d3ccb092936551fc", - "source": "Not active", - "translated": "Неактивно" - }, - { - "id": "native.apple.1620f7cb3deea133", - "source": "Open Gateway Settings", - "translated": "Открыть настройки Gateway" - }, - { - "id": "native.apple.3fd2706ef0f14fd2", - "source": "Open Voice Settings", - "translated": "Открыть настройки голоса" - }, - { - "id": "native.apple.3cc9cd23a67db077", - "source": "Demo Mode Only", - "translated": "Только деморежим" - }, - { - "id": "native.apple.12c9d8231777366c", - "source": "Waiting for Approval", - "translated": "Ожидает подтверждения" - }, { "id": "native.apple.dc57861a3ac5a08e", "source": "Open Settings", @@ -14528,6 +14448,41 @@ "source": "Action required", "translated": "Требуется действие" }, + { + "id": "native.apple.7205f0822706cb55", + "source": "Listening", + "translated": "Прослушивание" + }, + { + "id": "native.apple.8b10cb1dc21e42af", + "source": "Listening (PTT)", + "translated": "Прослушивание (PTT)" + }, + { + "id": "native.apple.939c7ec55636c67f", + "source": "Listening (Realtime)", + "translated": "Прослушивание (в реальном времени)" + }, + { + "id": "native.apple.86f203ebb2453d15", + "source": "Speaking…", + "translated": "Говорит…" + }, + { + "id": "native.apple.1e3ff6b56ae3828d", + "source": "Speaking (System)…", + "translated": "Воспроизведение речи (системное)…" + }, + { + "id": "native.apple.37a5fedaefb8ebf3", + "source": "Generating voice…", + "translated": "Создание голоса…" + }, + { + "id": "native.apple.47003f094346c660", + "source": "Ready", + "translated": "Готово" + }, { "id": "native.apple.cf0450b542893fb5", "source": "Off", @@ -15348,26 +15303,6 @@ "source": "unknown relay error", "translated": "unknown relay error" }, - { - "id": "native.apple.f2ea327bfea8b8e3", - "source": "Talk", - "translated": "Говорить" - }, - { - "id": "native.apple.bda6cfa988e49a75", - "source": "Control", - "translated": "Управление" - }, - { - "id": "native.apple.3c80f5351055a5f3", - "source": "Agent", - "translated": "Агент" - }, - { - "id": "native.apple.22d107db5b905ef4", - "source": "Settings", - "translated": "Настройки" - }, { "id": "native.apple.c23d35de8d70863f", "source": "OpenClaw", @@ -15458,6 +15393,26 @@ "source": "Close canvas", "translated": "Закрыть холст" }, + { + "id": "native.apple.a1db6ce0aed1d18e", + "source": "Chat, voice active", + "translated": "Чат, голос включён" + }, + { + "id": "native.apple.bda6cfa988e49a75", + "source": "Control", + "translated": "Управление" + }, + { + "id": "native.apple.3c80f5351055a5f3", + "source": "Agent", + "translated": "Агент" + }, + { + "id": "native.apple.22d107db5b905ef4", + "source": "Settings", + "translated": "Настройки" + }, { "id": "native.apple.8a4751c86aa6fe77", "source": "Gateway needs attention", @@ -15488,11 +15443,6 @@ "source": "Chat", "translated": "Чат" }, - { - "id": "native.apple.febca7528af94342", - "source": "Talk", - "translated": "Разговор" - }, { "id": "native.apple.e26bbe303d8a9544", "source": "Overview", @@ -15863,6 +15813,11 @@ "source": "Requesting permissions…", "translated": "Запрос разрешений…" }, + { + "id": "native.apple.198f59c97d7afe11", + "source": "Start failed: %@", + "translated": "Не удалось запустить: %@" + }, { "id": "native.apple.a1910b5c2e008b6c", "source": "Microphone permission denied", @@ -15873,11 +15828,6 @@ "source": "Speech recognition", "translated": "Распознавание речи" }, - { - "id": "native.apple.198f59c97d7afe11", - "source": "Start failed: %@", - "translated": "Не удалось запустить: %@" - }, { "id": "native.apple.c09f4e3bbef8177a", "source": "Offline", @@ -16088,66 +16038,6 @@ "source": "Listening (PTT)", "translated": "Прослушивание (PTT)" }, - { - "id": "native.apple.0189269b6a863dc2", - "source": "Request ID", - "translated": "ID запроса" - }, - { - "id": "native.apple.f9d69f27f9d7f4de", - "source": "Sending...", - "translated": "Отправка..." - }, - { - "id": "native.apple.0c5336129f70faed", - "source": "Retry", - "translated": "Повторить" - }, - { - "id": "native.apple.7b23f0a89d3ad411", - "source": "Sending approval request", - "translated": "Отправка запроса на подтверждение" - }, - { - "id": "native.apple.55db7317a2542e51", - "source": "Approval sent", - "translated": "Подтверждение отправлено" - }, - { - "id": "native.apple.06330f62ab254a0e", - "source": "Could not request approval", - "translated": "Не удалось запросить подтверждение" - }, - { - "id": "native.apple.6580d3d1cdda682d", - "source": "Enable Talk", - "translated": "Включить Talk" - }, - { - "id": "native.apple.abbfc1ba85a230b4", - "source": "Sending a new pairing request to your gateway...", - "translated": "Отправка нового запроса на сопряжение вашему gateway..." - }, - { - "id": "native.apple.2880deb53ea2fd86", - "source": "Approve this request on your gateway. Talk will start automatically when approval lands.", - "translated": "Подтвердите этот запрос на вашем gateway. Talk запустится автоматически, когда будет получено подтверждение." - }, - { - "id": "native.apple.80faa829f543c03c", - "source": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider.", - "translated": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider." - }, - { - "id": "native.apple.88b0a2e2986fcebf", - "source": "Request Again", - "translated": "Запросить снова" - }, - { - "id": "native.apple.5a3faae3472b4b86", - "source": "Send Approval Request", - "translated": "Отправить запрос на подтверждение" - }, { "id": "native.apple.1065cfe92d7c1eb6", "source": "Asking OpenClaw", @@ -22908,11 +22798,6 @@ "source": "Attachments", "translated": "Вложения" }, - { - "id": "native.apple.f8dda832ed76441d", - "source": "Voice note", - "translated": "Голосовое сообщение" - }, { "id": "native.apple.ee2c0d22f5861159", "source": "Talk", @@ -23898,6 +23783,11 @@ "source": "Dismiss", "translated": "Закрыть" }, + { + "id": "native.apple.3892b7f55d9d1f0a", + "source": "Remove attachments or wait for delivery to resolve before starting a new chat.", + "translated": "Удалите вложения или дождитесь доставки, прежде чем начинать новый чат." + }, { "id": "native.apple.92fe9093903eda3b", "source": "Could not attach voice note: %@", @@ -23958,11 +23848,6 @@ "source": "Gateway changed before the session operation started.", "translated": "Gateway изменился до начала операции с сеансом." }, - { - "id": "native.apple.d93e6fe16ec1b0e5", - "source": "Remove attachments or wait for delivery to resolve before starting a new chat.", - "translated": "Удалите вложения или дождитесь доставки, прежде чем начинать новый чат." - }, { "id": "native.apple.710c7bd117a7cc12", "source": "Remove attachments or wait for delivery to resolve before switching chats.", @@ -24188,6 +24073,61 @@ "source": "Compact Session", "translated": "Сжать сессию" }, + { + "id": "native.apple.fa10740f9e272e83", + "source": "Voice note", + "translated": "Голосовое сообщение" + }, + { + "id": "native.apple.e4de5afc37981d6c", + "source": "Photo Library", + "translated": "Медиатека" + }, + { + "id": "native.apple.37c19a66626b21fa", + "source": "Camera", + "translated": "Камера" + }, + { + "id": "native.apple.333682684163ba6b", + "source": "Choose Image File", + "translated": "Выбрать файл изображения" + }, + { + "id": "native.apple.534246792690968d", + "source": "Add attachment", + "translated": "Добавить вложение" + }, + { + "id": "native.apple.e33210feaa3da016", + "source": "Record Voice Note", + "translated": "Записать голосовое сообщение" + }, + { + "id": "native.apple.00f0e15858e38d19", + "source": "Dictate message", + "translated": "Продиктовать сообщение" + }, + { + "id": "native.apple.96f20175f61a2b29", + "source": "Listening", + "translated": "Прослушивание" + }, + { + "id": "native.apple.eaa9047db6cfb67e", + "source": "Not listening", + "translated": "Не слушает" + }, + { + "id": "native.apple.4185df7a940f09ef", + "source": "Finish dictation", + "translated": "Завершить диктовку" + }, + { + "id": "native.apple.5a0793d2827b9399", + "source": "Transcribe speech into the message", + "translated": "Преобразовать речь в текст сообщения." + }, { "id": "native.apple.f74d150ce05205dd", "source": "z", diff --git a/apps/.i18n/native/sv.json b/apps/.i18n/native/sv.json index 81dba1c6d49c..26f642b68116 100644 --- a/apps/.i18n/native/sv.json +++ b/apps/.i18n/native/sv.json @@ -8358,11 +8358,21 @@ "source": "OpenClaw Activity", "translated": "OpenClaw-aktivitet" }, + { + "id": "native.apple.093081590556db7f", + "source": "OPENCLAW", + "translated": "OPENCLAW" + }, { "id": "native.apple.86d546e1f1247028", "source": "OpenClaw", "translated": "OpenClaw" }, + { + "id": "native.apple.9fd01bdb5a753f3e", + "source": "LIVE", + "translated": "LIVE" + }, { "id": "native.apple.417ac9273b5f63ca", "source": "Connecting...", @@ -8383,6 +8393,36 @@ "source": "Action required", "translated": "Åtgärd krävs" }, + { + "id": "native.apple.275026f681242715", + "source": "Using %@", + "translated": "Med %@" + }, + { + "id": "native.apple.360d0d0fc0df9eb2", + "source": "Using a tool", + "translated": "Använder ett verktyg" + }, + { + "id": "native.apple.0d0ee660962ea840", + "source": "Listening", + "translated": "Lyssnar" + }, + { + "id": "native.apple.095ebf1496feb2ad", + "source": "Speaking", + "translated": "Talar" + }, + { + "id": "native.apple.63c98d2ca6a5b09c", + "source": "Voice active", + "translated": "Röst aktiv" + }, + { + "id": "native.apple.02470f4b5348f97c", + "source": "Paused", + "translated": "Pausade" + }, { "id": "native.apple.f2f3e6437fafcd1c", "source": "Connected", @@ -10518,11 +10558,36 @@ "source": "The session attaches once the gateway is ready.", "translated": "Sessionen ansluter när Gateway är redo." }, + { + "id": "native.apple.02611ddddef7295a", + "source": "Collapsed", + "translated": "Ihopfälld" + }, + { + "id": "native.apple.1f4911f4a05230b5", + "source": "Expanded", + "translated": "Utfälld" + }, { "id": "native.apple.d19c9577e0fdfea7", "source": "Opens Settings / Gateway", "translated": "Öppnar Inställningar / Gateway" }, + { + "id": "native.apple.b59e690104ae3e24", + "source": "Hides the gateway status label", + "translated": "Döljer gateway-statusetiketten" + }, + { + "id": "native.apple.5fcf9d462463dd27", + "source": "Shows the full gateway status label", + "translated": "Visar hela gateway-statusetiketten" + }, + { + "id": "native.apple.592b1e673e42df50", + "source": "Voice off", + "translated": "Röst av" + }, { "id": "native.apple.7c87384c7d1370e1", "source": "New Chat", @@ -10548,6 +10613,11 @@ "source": "Export Transcript", "translated": "Exportera transkript" }, + { + "id": "native.apple.d510e012c91b24ed", + "source": "Gateway Settings", + "translated": "Gateway-inställningar" + }, { "id": "native.apple.f26cef22f417abb4", "source": "Show reasoning & tool activity", @@ -11728,11 +11798,6 @@ "source": "Agent chat and recent work.", "translated": "Agentchatt och senaste arbete." }, - { - "id": "native.apple.a30d0a950d0a77aa", - "source": "Realtime voice and controls.", - "translated": "Realtidsröst och kontroller." - }, { "id": "native.apple.a03e961ab14ba060", "source": "Overview", @@ -13818,151 +13883,6 @@ "source": "ClawHub did not report an installable version for this skill.", "translated": "ClawHub rapporterade ingen installerbar version för denna skill." }, - { - "id": "native.apple.798c3584ad95f8da", - "source": "Enable Talk", - "translated": "Aktivera Talk" - }, - { - "id": "native.apple.c6e171128190d131", - "source": "Not Now", - "translated": "Inte nu" - }, - { - "id": "native.apple.e4b15aef3c2e3da7", - "source": "Talk", - "translated": "Prata" - }, - { - "id": "native.apple.ab3381742bdff650", - "source": "Conversation", - "translated": "Konversation" - }, - { - "id": "native.apple.7af100f6640c6a69", - "source": "Agent", - "translated": "Agent" - }, - { - "id": "native.apple.49c4de60117b1a98", - "source": "Session", - "translated": "Session" - }, - { - "id": "native.apple.37c29d2506b955e9", - "source": "Runtime", - "translated": "Körmiljö" - }, - { - "id": "native.apple.13e0ad79b141576e", - "source": "Voice Mode", - "translated": "Röstläge" - }, - { - "id": "native.apple.e8035b71bab833c7", - "source": "Configured", - "translated": "Konfigurerad" - }, - { - "id": "native.apple.1b2845ec89d20eef", - "source": "Active", - "translated": "Aktiv" - }, - { - "id": "native.apple.a82c07df64d13b23", - "source": "Transport", - "translated": "Transport" - }, - { - "id": "native.apple.384e097278f76954", - "source": "Last issue", - "translated": "Senaste problem" - }, - { - "id": "native.apple.a6d8fc7bfd98836f", - "source": "Permission", - "translated": "Behörighet" - }, - { - "id": "native.apple.983bdcd5e3f1baa8", - "source": "Speech language", - "translated": "Talspråk" - }, - { - "id": "native.apple.32e7fa2b1798e9c3", - "source": "Controls", - "translated": "Kontroller" - }, - { - "id": "native.apple.0eb012a5d3d81b32", - "source": "Speakerphone", - "translated": "Högtalartelefon" - }, - { - "id": "native.apple.ef8699e2718346da", - "source": "Background listening", - "translated": "Lyssning i bakgrunden" - }, - { - "id": "native.apple.1e6938a09b4fbe77", - "source": "Voice & Talk Settings", - "translated": "Inställningar för röst och samtal" - }, - { - "id": "native.apple.c7a591fd5bb6687e", - "source": "Gateway approval is required before this phone can capture voice.", - "translated": "Godkännande från Gateway krävs innan den här telefonen kan spela in röst." - }, - { - "id": "native.apple.9d40e75e55ed39eb", - "source": "Voice is disabled in Apple Review demo mode.", - "translated": "Röst är inaktiverat i demoläget för Apple Review." - }, - { - "id": "native.apple.3ef69bf916177799", - "source": "Connect to your gateway to start a voice conversation.", - "translated": "Anslut till din Gateway för att starta ett röstsamtal." - }, - { - "id": "native.apple.ed03af7c68841a5d", - "source": "Open Voice settings after the gateway loads Talk configuration.", - "translated": "Öppna röstinställningarna efter att Gateway har läst in Talk-konfigurationen." - }, - { - "id": "native.apple.883a251978601b96", - "source": "Routes voice to %@.", - "translated": "Dirigerar röst till %@." - }, - { - "id": "native.apple.877fb5c1abfaafa1", - "source": "Not loaded", - "translated": "Not loaded" - }, - { - "id": "native.apple.d3ccb092936551fc", - "source": "Not active", - "translated": "Inte aktiv" - }, - { - "id": "native.apple.1620f7cb3deea133", - "source": "Open Gateway Settings", - "translated": "Öppna Gateway-inställningar" - }, - { - "id": "native.apple.3fd2706ef0f14fd2", - "source": "Open Voice Settings", - "translated": "Öppna röstinställningar" - }, - { - "id": "native.apple.3cc9cd23a67db077", - "source": "Demo Mode Only", - "translated": "Endast demoläge" - }, - { - "id": "native.apple.12c9d8231777366c", - "source": "Waiting for Approval", - "translated": "Väntar på godkännande" - }, { "id": "native.apple.dc57861a3ac5a08e", "source": "Open Settings", @@ -14528,6 +14448,41 @@ "source": "Action required", "translated": "Åtgärd krävs" }, + { + "id": "native.apple.7205f0822706cb55", + "source": "Listening", + "translated": "Lyssnar" + }, + { + "id": "native.apple.8b10cb1dc21e42af", + "source": "Listening (PTT)", + "translated": "Lyssnar (PTT)" + }, + { + "id": "native.apple.939c7ec55636c67f", + "source": "Listening (Realtime)", + "translated": "Lyssnar (realtid)" + }, + { + "id": "native.apple.86f203ebb2453d15", + "source": "Speaking…", + "translated": "Talar…" + }, + { + "id": "native.apple.1e3ff6b56ae3828d", + "source": "Speaking (System)…", + "translated": "Talar (system)…" + }, + { + "id": "native.apple.37a5fedaefb8ebf3", + "source": "Generating voice…", + "translated": "Genererar röst…" + }, + { + "id": "native.apple.47003f094346c660", + "source": "Ready", + "translated": "Klar" + }, { "id": "native.apple.cf0450b542893fb5", "source": "Off", @@ -15348,26 +15303,6 @@ "source": "unknown relay error", "translated": "unknown relay error" }, - { - "id": "native.apple.f2ea327bfea8b8e3", - "source": "Talk", - "translated": "Prata" - }, - { - "id": "native.apple.bda6cfa988e49a75", - "source": "Control", - "translated": "Kontroll" - }, - { - "id": "native.apple.3c80f5351055a5f3", - "source": "Agent", - "translated": "Agent" - }, - { - "id": "native.apple.22d107db5b905ef4", - "source": "Settings", - "translated": "Inställningar" - }, { "id": "native.apple.c23d35de8d70863f", "source": "OpenClaw", @@ -15458,6 +15393,26 @@ "source": "Close canvas", "translated": "Stäng canvas" }, + { + "id": "native.apple.a1db6ce0aed1d18e", + "source": "Chat, voice active", + "translated": "Chatt, röst aktiv" + }, + { + "id": "native.apple.bda6cfa988e49a75", + "source": "Control", + "translated": "Kontroll" + }, + { + "id": "native.apple.3c80f5351055a5f3", + "source": "Agent", + "translated": "Agent" + }, + { + "id": "native.apple.22d107db5b905ef4", + "source": "Settings", + "translated": "Inställningar" + }, { "id": "native.apple.8a4751c86aa6fe77", "source": "Gateway needs attention", @@ -15488,11 +15443,6 @@ "source": "Chat", "translated": "Chatt" }, - { - "id": "native.apple.febca7528af94342", - "source": "Talk", - "translated": "Prata" - }, { "id": "native.apple.e26bbe303d8a9544", "source": "Overview", @@ -15863,6 +15813,11 @@ "source": "Requesting permissions…", "translated": "Begär behörigheter…" }, + { + "id": "native.apple.198f59c97d7afe11", + "source": "Start failed: %@", + "translated": "Starten misslyckades: %@" + }, { "id": "native.apple.a1910b5c2e008b6c", "source": "Microphone permission denied", @@ -15873,11 +15828,6 @@ "source": "Speech recognition", "translated": "Taligenkänning" }, - { - "id": "native.apple.198f59c97d7afe11", - "source": "Start failed: %@", - "translated": "Starten misslyckades: %@" - }, { "id": "native.apple.c09f4e3bbef8177a", "source": "Offline", @@ -16088,66 +16038,6 @@ "source": "Listening (PTT)", "translated": "Lyssnar (PTT)" }, - { - "id": "native.apple.0189269b6a863dc2", - "source": "Request ID", - "translated": "Förfrågnings-ID" - }, - { - "id": "native.apple.f9d69f27f9d7f4de", - "source": "Sending...", - "translated": "Skickar..." - }, - { - "id": "native.apple.0c5336129f70faed", - "source": "Retry", - "translated": "Försök igen" - }, - { - "id": "native.apple.7b23f0a89d3ad411", - "source": "Sending approval request", - "translated": "Skickar godkännandeförfrågan" - }, - { - "id": "native.apple.55db7317a2542e51", - "source": "Approval sent", - "translated": "Godkännande skickat" - }, - { - "id": "native.apple.06330f62ab254a0e", - "source": "Could not request approval", - "translated": "Kunde inte begära godkännande" - }, - { - "id": "native.apple.6580d3d1cdda682d", - "source": "Enable Talk", - "translated": "Aktivera Talk" - }, - { - "id": "native.apple.abbfc1ba85a230b4", - "source": "Sending a new pairing request to your gateway...", - "translated": "Skickar en ny parkopplingsförfrågan till din gateway..." - }, - { - "id": "native.apple.2880deb53ea2fd86", - "source": "Approve this request on your gateway. Talk will start automatically when approval lands.", - "translated": "Godkänn den här förfrågan på din gateway. Talk startar automatiskt när godkännandet tas emot." - }, - { - "id": "native.apple.80faa829f543c03c", - "source": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider.", - "translated": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider." - }, - { - "id": "native.apple.88b0a2e2986fcebf", - "source": "Request Again", - "translated": "Begär igen" - }, - { - "id": "native.apple.5a3faae3472b4b86", - "source": "Send Approval Request", - "translated": "Skicka godkännandeförfrågan" - }, { "id": "native.apple.1065cfe92d7c1eb6", "source": "Asking OpenClaw", @@ -22908,11 +22798,6 @@ "source": "Attachments", "translated": "Bilagor" }, - { - "id": "native.apple.f8dda832ed76441d", - "source": "Voice note", - "translated": "Röstmeddelande" - }, { "id": "native.apple.ee2c0d22f5861159", "source": "Talk", @@ -23898,6 +23783,11 @@ "source": "Dismiss", "translated": "Avvisa" }, + { + "id": "native.apple.3892b7f55d9d1f0a", + "source": "Remove attachments or wait for delivery to resolve before starting a new chat.", + "translated": "Ta bort bilagor eller vänta på att leveransen slutförs innan du startar en ny chatt." + }, { "id": "native.apple.92fe9093903eda3b", "source": "Could not attach voice note: %@", @@ -23958,11 +23848,6 @@ "source": "Gateway changed before the session operation started.", "translated": "Gateway ändrades innan sessionsåtgärden startade." }, - { - "id": "native.apple.d93e6fe16ec1b0e5", - "source": "Remove attachments or wait for delivery to resolve before starting a new chat.", - "translated": "Ta bort bilagor eller vänta på att leveransen slutförs innan du startar en ny chatt." - }, { "id": "native.apple.710c7bd117a7cc12", "source": "Remove attachments or wait for delivery to resolve before switching chats.", @@ -24188,6 +24073,61 @@ "source": "Compact Session", "translated": "Komprimera session" }, + { + "id": "native.apple.fa10740f9e272e83", + "source": "Voice note", + "translated": "Röstmeddelande" + }, + { + "id": "native.apple.e4de5afc37981d6c", + "source": "Photo Library", + "translated": "Bildbibliotek" + }, + { + "id": "native.apple.37c19a66626b21fa", + "source": "Camera", + "translated": "Kamera" + }, + { + "id": "native.apple.333682684163ba6b", + "source": "Choose Image File", + "translated": "Välj bildfil" + }, + { + "id": "native.apple.534246792690968d", + "source": "Add attachment", + "translated": "Lägg till bilaga" + }, + { + "id": "native.apple.e33210feaa3da016", + "source": "Record Voice Note", + "translated": "Spela in röstmeddelande" + }, + { + "id": "native.apple.00f0e15858e38d19", + "source": "Dictate message", + "translated": "Diktera meddelande" + }, + { + "id": "native.apple.96f20175f61a2b29", + "source": "Listening", + "translated": "Lyssnar" + }, + { + "id": "native.apple.eaa9047db6cfb67e", + "source": "Not listening", + "translated": "Lyssnar inte" + }, + { + "id": "native.apple.4185df7a940f09ef", + "source": "Finish dictation", + "translated": "Avsluta diktering" + }, + { + "id": "native.apple.5a0793d2827b9399", + "source": "Transcribe speech into the message", + "translated": "Omvandla tal till text i meddelandet." + }, { "id": "native.apple.f74d150ce05205dd", "source": "z", diff --git a/apps/.i18n/native/th.json b/apps/.i18n/native/th.json index 523f77cb869d..dd558d88f80f 100644 --- a/apps/.i18n/native/th.json +++ b/apps/.i18n/native/th.json @@ -8358,11 +8358,21 @@ "source": "OpenClaw Activity", "translated": "กิจกรรม OpenClaw" }, + { + "id": "native.apple.093081590556db7f", + "source": "OPENCLAW", + "translated": "OPENCLAW" + }, { "id": "native.apple.86d546e1f1247028", "source": "OpenClaw", "translated": "OpenClaw" }, + { + "id": "native.apple.9fd01bdb5a753f3e", + "source": "LIVE", + "translated": "LIVE" + }, { "id": "native.apple.417ac9273b5f63ca", "source": "Connecting...", @@ -8383,6 +8393,36 @@ "source": "Action required", "translated": "ต้องดำเนินการ" }, + { + "id": "native.apple.275026f681242715", + "source": "Using %@", + "translated": "ใช้ %@" + }, + { + "id": "native.apple.360d0d0fc0df9eb2", + "source": "Using a tool", + "translated": "ใช้เครื่องมือ" + }, + { + "id": "native.apple.0d0ee660962ea840", + "source": "Listening", + "translated": "กำลังฟัง" + }, + { + "id": "native.apple.095ebf1496feb2ad", + "source": "Speaking", + "translated": "กำลังพูด" + }, + { + "id": "native.apple.63c98d2ca6a5b09c", + "source": "Voice active", + "translated": "เสียงเปิดอยู่" + }, + { + "id": "native.apple.02470f4b5348f97c", + "source": "Paused", + "translated": "หยุดชั่วคราว" + }, { "id": "native.apple.f2f3e6437fafcd1c", "source": "Connected", @@ -10518,11 +10558,36 @@ "source": "The session attaches once the gateway is ready.", "translated": "เซสชันจะเชื่อมต่อเมื่อ gateway พร้อมใช้งาน" }, + { + "id": "native.apple.02611ddddef7295a", + "source": "Collapsed", + "translated": "ยุบอยู่" + }, + { + "id": "native.apple.1f4911f4a05230b5", + "source": "Expanded", + "translated": "ขยายอยู่" + }, { "id": "native.apple.d19c9577e0fdfea7", "source": "Opens Settings / Gateway", "translated": "เปิด Settings / Gateway" }, + { + "id": "native.apple.b59e690104ae3e24", + "source": "Hides the gateway status label", + "translated": "ซ่อนป้ายสถานะเกตเวย์" + }, + { + "id": "native.apple.5fcf9d462463dd27", + "source": "Shows the full gateway status label", + "translated": "แสดงป้ายสถานะเกตเวย์แบบเต็ม" + }, + { + "id": "native.apple.592b1e673e42df50", + "source": "Voice off", + "translated": "เสียงปิดอยู่" + }, { "id": "native.apple.7c87384c7d1370e1", "source": "New Chat", @@ -10548,6 +10613,11 @@ "source": "Export Transcript", "translated": "ส่งออกบันทึกการสนทนา" }, + { + "id": "native.apple.d510e012c91b24ed", + "source": "Gateway Settings", + "translated": "การตั้งค่าเกตเวย์" + }, { "id": "native.apple.f26cef22f417abb4", "source": "Show reasoning & tool activity", @@ -11728,11 +11798,6 @@ "source": "Agent chat and recent work.", "translated": "แชทกับเอเจนต์และงานล่าสุด" }, - { - "id": "native.apple.a30d0a950d0a77aa", - "source": "Realtime voice and controls.", - "translated": "เสียงและการควบคุมแบบเรียลไทม์" - }, { "id": "native.apple.a03e961ab14ba060", "source": "Overview", @@ -13818,151 +13883,6 @@ "source": "ClawHub did not report an installable version for this skill.", "translated": "ClawHub ไม่ได้รายงานเวอร์ชันที่ติดตั้งได้สำหรับ Skill นี้" }, - { - "id": "native.apple.798c3584ad95f8da", - "source": "Enable Talk", - "translated": "เปิดใช้งาน Talk" - }, - { - "id": "native.apple.c6e171128190d131", - "source": "Not Now", - "translated": "ไม่ใช่ตอนนี้" - }, - { - "id": "native.apple.e4b15aef3c2e3da7", - "source": "Talk", - "translated": "พูดคุย" - }, - { - "id": "native.apple.ab3381742bdff650", - "source": "Conversation", - "translated": "การสนทนา" - }, - { - "id": "native.apple.7af100f6640c6a69", - "source": "Agent", - "translated": "เอเจนต์" - }, - { - "id": "native.apple.49c4de60117b1a98", - "source": "Session", - "translated": "เซสชัน" - }, - { - "id": "native.apple.37c29d2506b955e9", - "source": "Runtime", - "translated": "รันไทม์" - }, - { - "id": "native.apple.13e0ad79b141576e", - "source": "Voice Mode", - "translated": "โหมดเสียง" - }, - { - "id": "native.apple.e8035b71bab833c7", - "source": "Configured", - "translated": "กำหนดค่าแล้ว" - }, - { - "id": "native.apple.1b2845ec89d20eef", - "source": "Active", - "translated": "ใช้งานอยู่" - }, - { - "id": "native.apple.a82c07df64d13b23", - "source": "Transport", - "translated": "การส่งข้อมูล" - }, - { - "id": "native.apple.384e097278f76954", - "source": "Last issue", - "translated": "ปัญหาล่าสุด" - }, - { - "id": "native.apple.a6d8fc7bfd98836f", - "source": "Permission", - "translated": "สิทธิ์อนุญาต" - }, - { - "id": "native.apple.983bdcd5e3f1baa8", - "source": "Speech language", - "translated": "ภาษาพูด" - }, - { - "id": "native.apple.32e7fa2b1798e9c3", - "source": "Controls", - "translated": "การควบคุม" - }, - { - "id": "native.apple.0eb012a5d3d81b32", - "source": "Speakerphone", - "translated": "ลำโพงโทรศัพท์" - }, - { - "id": "native.apple.ef8699e2718346da", - "source": "Background listening", - "translated": "การฟังในพื้นหลัง" - }, - { - "id": "native.apple.1e6938a09b4fbe77", - "source": "Voice & Talk Settings", - "translated": "การตั้งค่าเสียงและการพูด" - }, - { - "id": "native.apple.c7a591fd5bb6687e", - "source": "Gateway approval is required before this phone can capture voice.", - "translated": "ต้องได้รับการอนุมัติจาก Gateway ก่อนที่โทรศัพท์เครื่องนี้จะสามารถบันทึกเสียงได้" - }, - { - "id": "native.apple.9d40e75e55ed39eb", - "source": "Voice is disabled in Apple Review demo mode.", - "translated": "เสียงถูกปิดใช้งานในโหมดสาธิตสำหรับ Apple Review" - }, - { - "id": "native.apple.3ef69bf916177799", - "source": "Connect to your gateway to start a voice conversation.", - "translated": "เชื่อมต่อกับ Gateway ของคุณเพื่อเริ่มการสนทนาด้วยเสียง" - }, - { - "id": "native.apple.ed03af7c68841a5d", - "source": "Open Voice settings after the gateway loads Talk configuration.", - "translated": "เปิดการตั้งค่าเสียงหลังจาก Gateway โหลดการกำหนดค่า Talk แล้ว" - }, - { - "id": "native.apple.883a251978601b96", - "source": "Routes voice to %@.", - "translated": "กำหนดเส้นทางเสียงไปยัง %@." - }, - { - "id": "native.apple.877fb5c1abfaafa1", - "source": "Not loaded", - "translated": "Not loaded" - }, - { - "id": "native.apple.d3ccb092936551fc", - "source": "Not active", - "translated": "ไม่ได้ใช้งาน" - }, - { - "id": "native.apple.1620f7cb3deea133", - "source": "Open Gateway Settings", - "translated": "เปิดการตั้งค่า Gateway" - }, - { - "id": "native.apple.3fd2706ef0f14fd2", - "source": "Open Voice Settings", - "translated": "เปิดการตั้งค่าเสียง" - }, - { - "id": "native.apple.3cc9cd23a67db077", - "source": "Demo Mode Only", - "translated": "โหมดสาธิตเท่านั้น" - }, - { - "id": "native.apple.12c9d8231777366c", - "source": "Waiting for Approval", - "translated": "กำลังรอการอนุมัติ" - }, { "id": "native.apple.dc57861a3ac5a08e", "source": "Open Settings", @@ -14528,6 +14448,41 @@ "source": "Action required", "translated": "ต้องดำเนินการ" }, + { + "id": "native.apple.7205f0822706cb55", + "source": "Listening", + "translated": "กำลังฟัง" + }, + { + "id": "native.apple.8b10cb1dc21e42af", + "source": "Listening (PTT)", + "translated": "กำลังฟัง (PTT)" + }, + { + "id": "native.apple.939c7ec55636c67f", + "source": "Listening (Realtime)", + "translated": "กำลังฟัง (เรียลไทม์)" + }, + { + "id": "native.apple.86f203ebb2453d15", + "source": "Speaking…", + "translated": "กำลังพูด…" + }, + { + "id": "native.apple.1e3ff6b56ae3828d", + "source": "Speaking (System)…", + "translated": "กำลังพูด (ระบบ)…" + }, + { + "id": "native.apple.37a5fedaefb8ebf3", + "source": "Generating voice…", + "translated": "กำลังสร้างเสียง…" + }, + { + "id": "native.apple.47003f094346c660", + "source": "Ready", + "translated": "พร้อม" + }, { "id": "native.apple.cf0450b542893fb5", "source": "Off", @@ -15348,26 +15303,6 @@ "source": "unknown relay error", "translated": "unknown relay error" }, - { - "id": "native.apple.f2ea327bfea8b8e3", - "source": "Talk", - "translated": "พูดคุย" - }, - { - "id": "native.apple.bda6cfa988e49a75", - "source": "Control", - "translated": "ควบคุม" - }, - { - "id": "native.apple.3c80f5351055a5f3", - "source": "Agent", - "translated": "Agent" - }, - { - "id": "native.apple.22d107db5b905ef4", - "source": "Settings", - "translated": "การตั้งค่า" - }, { "id": "native.apple.c23d35de8d70863f", "source": "OpenClaw", @@ -15458,6 +15393,26 @@ "source": "Close canvas", "translated": "ปิดแคนวาส" }, + { + "id": "native.apple.a1db6ce0aed1d18e", + "source": "Chat, voice active", + "translated": "แชท เปิดเสียงอยู่" + }, + { + "id": "native.apple.bda6cfa988e49a75", + "source": "Control", + "translated": "ควบคุม" + }, + { + "id": "native.apple.3c80f5351055a5f3", + "source": "Agent", + "translated": "Agent" + }, + { + "id": "native.apple.22d107db5b905ef4", + "source": "Settings", + "translated": "การตั้งค่า" + }, { "id": "native.apple.8a4751c86aa6fe77", "source": "Gateway needs attention", @@ -15488,11 +15443,6 @@ "source": "Chat", "translated": "แชท" }, - { - "id": "native.apple.febca7528af94342", - "source": "Talk", - "translated": "พูดคุย" - }, { "id": "native.apple.e26bbe303d8a9544", "source": "Overview", @@ -15863,6 +15813,11 @@ "source": "Requesting permissions…", "translated": "กำลังขอสิทธิ์…" }, + { + "id": "native.apple.198f59c97d7afe11", + "source": "Start failed: %@", + "translated": "เริ่มไม่สำเร็จ: %@" + }, { "id": "native.apple.a1910b5c2e008b6c", "source": "Microphone permission denied", @@ -15873,11 +15828,6 @@ "source": "Speech recognition", "translated": "การรู้จำเสียงพูด" }, - { - "id": "native.apple.198f59c97d7afe11", - "source": "Start failed: %@", - "translated": "เริ่มไม่สำเร็จ: %@" - }, { "id": "native.apple.c09f4e3bbef8177a", "source": "Offline", @@ -16088,66 +16038,6 @@ "source": "Listening (PTT)", "translated": "กำลังฟัง (PTT)" }, - { - "id": "native.apple.0189269b6a863dc2", - "source": "Request ID", - "translated": "ID คำขอ" - }, - { - "id": "native.apple.f9d69f27f9d7f4de", - "source": "Sending...", - "translated": "กำลังส่ง..." - }, - { - "id": "native.apple.0c5336129f70faed", - "source": "Retry", - "translated": "ลองอีกครั้ง" - }, - { - "id": "native.apple.7b23f0a89d3ad411", - "source": "Sending approval request", - "translated": "กำลังส่งคำขออนุมัติ" - }, - { - "id": "native.apple.55db7317a2542e51", - "source": "Approval sent", - "translated": "ส่งการอนุมัติแล้ว" - }, - { - "id": "native.apple.06330f62ab254a0e", - "source": "Could not request approval", - "translated": "ไม่สามารถขออนุมัติได้" - }, - { - "id": "native.apple.6580d3d1cdda682d", - "source": "Enable Talk", - "translated": "เปิดใช้งาน Talk" - }, - { - "id": "native.apple.abbfc1ba85a230b4", - "source": "Sending a new pairing request to your gateway...", - "translated": "กำลังส่งคำขอจับคู่ใหม่ไปยัง gateway ของคุณ..." - }, - { - "id": "native.apple.2880deb53ea2fd86", - "source": "Approve this request on your gateway. Talk will start automatically when approval lands.", - "translated": "อนุมัติคำขอนี้บน gateway ของคุณ Talk จะเริ่มโดยอัตโนมัติเมื่อได้รับการอนุมัติ" - }, - { - "id": "native.apple.80faa829f543c03c", - "source": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider.", - "translated": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider." - }, - { - "id": "native.apple.88b0a2e2986fcebf", - "source": "Request Again", - "translated": "ขออีกครั้ง" - }, - { - "id": "native.apple.5a3faae3472b4b86", - "source": "Send Approval Request", - "translated": "ส่งคำขออนุมัติ" - }, { "id": "native.apple.1065cfe92d7c1eb6", "source": "Asking OpenClaw", @@ -22908,11 +22798,6 @@ "source": "Attachments", "translated": "ไฟล์แนบ" }, - { - "id": "native.apple.f8dda832ed76441d", - "source": "Voice note", - "translated": "บันทึกเสียง" - }, { "id": "native.apple.ee2c0d22f5861159", "source": "Talk", @@ -23898,6 +23783,11 @@ "source": "Dismiss", "translated": "ปิด" }, + { + "id": "native.apple.3892b7f55d9d1f0a", + "source": "Remove attachments or wait for delivery to resolve before starting a new chat.", + "translated": "ลบไฟล์แนบหรือรอให้การส่งเสร็จสิ้นก่อนเริ่มแชทใหม่" + }, { "id": "native.apple.92fe9093903eda3b", "source": "Could not attach voice note: %@", @@ -23958,11 +23848,6 @@ "source": "Gateway changed before the session operation started.", "translated": "Gateway มีการเปลี่ยนแปลงก่อนเริ่มดำเนินการกับเซสชัน" }, - { - "id": "native.apple.d93e6fe16ec1b0e5", - "source": "Remove attachments or wait for delivery to resolve before starting a new chat.", - "translated": "ลบไฟล์แนบหรือรอให้การส่งเสร็จสิ้นก่อนเริ่มแชทใหม่" - }, { "id": "native.apple.710c7bd117a7cc12", "source": "Remove attachments or wait for delivery to resolve before switching chats.", @@ -24188,6 +24073,61 @@ "source": "Compact Session", "translated": "ย่อเซสชัน" }, + { + "id": "native.apple.fa10740f9e272e83", + "source": "Voice note", + "translated": "ข้อความเสียง" + }, + { + "id": "native.apple.e4de5afc37981d6c", + "source": "Photo Library", + "translated": "คลังรูปภาพ" + }, + { + "id": "native.apple.37c19a66626b21fa", + "source": "Camera", + "translated": "กล้อง" + }, + { + "id": "native.apple.333682684163ba6b", + "source": "Choose Image File", + "translated": "เลือกรูปภาพ" + }, + { + "id": "native.apple.534246792690968d", + "source": "Add attachment", + "translated": "เพิ่มไฟล์แนบ" + }, + { + "id": "native.apple.e33210feaa3da016", + "source": "Record Voice Note", + "translated": "บันทึกข้อความเสียง" + }, + { + "id": "native.apple.00f0e15858e38d19", + "source": "Dictate message", + "translated": "พูดป้อนข้อความ" + }, + { + "id": "native.apple.96f20175f61a2b29", + "source": "Listening", + "translated": "กำลังฟัง" + }, + { + "id": "native.apple.eaa9047db6cfb67e", + "source": "Not listening", + "translated": "ไม่ได้ฟังอยู่" + }, + { + "id": "native.apple.4185df7a940f09ef", + "source": "Finish dictation", + "translated": "เสร็จสิ้นการป้อนด้วยเสียง" + }, + { + "id": "native.apple.5a0793d2827b9399", + "source": "Transcribe speech into the message", + "translated": "ถอดเสียงพูดเป็นข้อความในข้อความ." + }, { "id": "native.apple.f74d150ce05205dd", "source": "z", diff --git a/apps/.i18n/native/tr.json b/apps/.i18n/native/tr.json index d44428f25642..dda60cb9550f 100644 --- a/apps/.i18n/native/tr.json +++ b/apps/.i18n/native/tr.json @@ -8358,11 +8358,21 @@ "source": "OpenClaw Activity", "translated": "OpenClaw Etkinliği" }, + { + "id": "native.apple.093081590556db7f", + "source": "OPENCLAW", + "translated": "OPENCLAW" + }, { "id": "native.apple.86d546e1f1247028", "source": "OpenClaw", "translated": "OpenClaw" }, + { + "id": "native.apple.9fd01bdb5a753f3e", + "source": "LIVE", + "translated": "LIVE" + }, { "id": "native.apple.417ac9273b5f63ca", "source": "Connecting...", @@ -8383,6 +8393,36 @@ "source": "Action required", "translated": "İşlem gerekli" }, + { + "id": "native.apple.275026f681242715", + "source": "Using %@", + "translated": "%@ kullanılıyor" + }, + { + "id": "native.apple.360d0d0fc0df9eb2", + "source": "Using a tool", + "translated": "Bir araç kullanılıyor" + }, + { + "id": "native.apple.0d0ee660962ea840", + "source": "Listening", + "translated": "Dinleniyor" + }, + { + "id": "native.apple.095ebf1496feb2ad", + "source": "Speaking", + "translated": "Konuşuyor" + }, + { + "id": "native.apple.63c98d2ca6a5b09c", + "source": "Voice active", + "translated": "Ses açık" + }, + { + "id": "native.apple.02470f4b5348f97c", + "source": "Paused", + "translated": "Duraklatıldı" + }, { "id": "native.apple.f2f3e6437fafcd1c", "source": "Connected", @@ -10518,11 +10558,36 @@ "source": "The session attaches once the gateway is ready.", "translated": "Gateway hazır olduğunda oturum bağlanır." }, + { + "id": "native.apple.02611ddddef7295a", + "source": "Collapsed", + "translated": "Daraltılmış" + }, + { + "id": "native.apple.1f4911f4a05230b5", + "source": "Expanded", + "translated": "Genişletilmiş" + }, { "id": "native.apple.d19c9577e0fdfea7", "source": "Opens Settings / Gateway", "translated": "Ayarlar / Gateway’i açar" }, + { + "id": "native.apple.b59e690104ae3e24", + "source": "Hides the gateway status label", + "translated": "Ağ geçidi durum etiketini gizler" + }, + { + "id": "native.apple.5fcf9d462463dd27", + "source": "Shows the full gateway status label", + "translated": "Tam ağ geçidi durum etiketini gösterir" + }, + { + "id": "native.apple.592b1e673e42df50", + "source": "Voice off", + "translated": "Ses kapalı" + }, { "id": "native.apple.7c87384c7d1370e1", "source": "New Chat", @@ -10548,6 +10613,11 @@ "source": "Export Transcript", "translated": "Dökümü dışa aktar" }, + { + "id": "native.apple.d510e012c91b24ed", + "source": "Gateway Settings", + "translated": "Ağ Geçidi Ayarları" + }, { "id": "native.apple.f26cef22f417abb4", "source": "Show reasoning & tool activity", @@ -11728,11 +11798,6 @@ "source": "Agent chat and recent work.", "translated": "Aracı sohbeti ve son çalışmalar." }, - { - "id": "native.apple.a30d0a950d0a77aa", - "source": "Realtime voice and controls.", - "translated": "Gerçek zamanlı ses ve kontroller." - }, { "id": "native.apple.a03e961ab14ba060", "source": "Overview", @@ -13818,151 +13883,6 @@ "source": "ClawHub did not report an installable version for this skill.", "translated": "ClawHub bu skill için yüklenebilir bir sürüm bildirmedi." }, - { - "id": "native.apple.798c3584ad95f8da", - "source": "Enable Talk", - "translated": "Konuşmayı Etkinleştir" - }, - { - "id": "native.apple.c6e171128190d131", - "source": "Not Now", - "translated": "Şimdi Değil" - }, - { - "id": "native.apple.e4b15aef3c2e3da7", - "source": "Talk", - "translated": "Konuş" - }, - { - "id": "native.apple.ab3381742bdff650", - "source": "Conversation", - "translated": "Sohbet" - }, - { - "id": "native.apple.7af100f6640c6a69", - "source": "Agent", - "translated": "Aracı" - }, - { - "id": "native.apple.49c4de60117b1a98", - "source": "Session", - "translated": "Oturum" - }, - { - "id": "native.apple.37c29d2506b955e9", - "source": "Runtime", - "translated": "Çalışma Zamanı" - }, - { - "id": "native.apple.13e0ad79b141576e", - "source": "Voice Mode", - "translated": "Ses Modu" - }, - { - "id": "native.apple.e8035b71bab833c7", - "source": "Configured", - "translated": "Yapılandırıldı" - }, - { - "id": "native.apple.1b2845ec89d20eef", - "source": "Active", - "translated": "Etkin" - }, - { - "id": "native.apple.a82c07df64d13b23", - "source": "Transport", - "translated": "Aktarım" - }, - { - "id": "native.apple.384e097278f76954", - "source": "Last issue", - "translated": "Son sorun" - }, - { - "id": "native.apple.a6d8fc7bfd98836f", - "source": "Permission", - "translated": "İzin" - }, - { - "id": "native.apple.983bdcd5e3f1baa8", - "source": "Speech language", - "translated": "Konuşma dili" - }, - { - "id": "native.apple.32e7fa2b1798e9c3", - "source": "Controls", - "translated": "Denetimler" - }, - { - "id": "native.apple.0eb012a5d3d81b32", - "source": "Speakerphone", - "translated": "Hoparlör" - }, - { - "id": "native.apple.ef8699e2718346da", - "source": "Background listening", - "translated": "Arka planda dinleme" - }, - { - "id": "native.apple.1e6938a09b4fbe77", - "source": "Voice & Talk Settings", - "translated": "Ses ve Konuşma Ayarları" - }, - { - "id": "native.apple.c7a591fd5bb6687e", - "source": "Gateway approval is required before this phone can capture voice.", - "translated": "Bu telefonun ses yakalayabilmesi için Gateway onayı gereklidir." - }, - { - "id": "native.apple.9d40e75e55ed39eb", - "source": "Voice is disabled in Apple Review demo mode.", - "translated": "Apple Review demo modunda ses devre dışıdır." - }, - { - "id": "native.apple.3ef69bf916177799", - "source": "Connect to your gateway to start a voice conversation.", - "translated": "Sesli görüşme başlatmak için Gateway'inize bağlanın." - }, - { - "id": "native.apple.ed03af7c68841a5d", - "source": "Open Voice settings after the gateway loads Talk configuration.", - "translated": "Gateway, Talk yapılandırmasını yükledikten sonra Ses ayarlarını açın." - }, - { - "id": "native.apple.883a251978601b96", - "source": "Routes voice to %@.", - "translated": "Sesi %@ hedefine yönlendirir." - }, - { - "id": "native.apple.877fb5c1abfaafa1", - "source": "Not loaded", - "translated": "Not loaded" - }, - { - "id": "native.apple.d3ccb092936551fc", - "source": "Not active", - "translated": "Etkin değil" - }, - { - "id": "native.apple.1620f7cb3deea133", - "source": "Open Gateway Settings", - "translated": "Gateway Ayarlarını Aç" - }, - { - "id": "native.apple.3fd2706ef0f14fd2", - "source": "Open Voice Settings", - "translated": "Ses Ayarlarını Aç" - }, - { - "id": "native.apple.3cc9cd23a67db077", - "source": "Demo Mode Only", - "translated": "Yalnızca Demo Modu" - }, - { - "id": "native.apple.12c9d8231777366c", - "source": "Waiting for Approval", - "translated": "Onay Bekleniyor" - }, { "id": "native.apple.dc57861a3ac5a08e", "source": "Open Settings", @@ -14528,6 +14448,41 @@ "source": "Action required", "translated": "İşlem gerekli" }, + { + "id": "native.apple.7205f0822706cb55", + "source": "Listening", + "translated": "Dinleniyor" + }, + { + "id": "native.apple.8b10cb1dc21e42af", + "source": "Listening (PTT)", + "translated": "Dinleniyor (PTT)" + }, + { + "id": "native.apple.939c7ec55636c67f", + "source": "Listening (Realtime)", + "translated": "Dinliyor (Gerçek Zamanlı)" + }, + { + "id": "native.apple.86f203ebb2453d15", + "source": "Speaking…", + "translated": "Konuşuyor…" + }, + { + "id": "native.apple.1e3ff6b56ae3828d", + "source": "Speaking (System)…", + "translated": "Konuşuyor (Sistem)…" + }, + { + "id": "native.apple.37a5fedaefb8ebf3", + "source": "Generating voice…", + "translated": "Ses oluşturuluyor…" + }, + { + "id": "native.apple.47003f094346c660", + "source": "Ready", + "translated": "Hazır" + }, { "id": "native.apple.cf0450b542893fb5", "source": "Off", @@ -15348,26 +15303,6 @@ "source": "unknown relay error", "translated": "unknown relay error" }, - { - "id": "native.apple.f2ea327bfea8b8e3", - "source": "Talk", - "translated": "Konuş" - }, - { - "id": "native.apple.bda6cfa988e49a75", - "source": "Control", - "translated": "Kontrol" - }, - { - "id": "native.apple.3c80f5351055a5f3", - "source": "Agent", - "translated": "Aracı" - }, - { - "id": "native.apple.22d107db5b905ef4", - "source": "Settings", - "translated": "Ayarlar" - }, { "id": "native.apple.c23d35de8d70863f", "source": "OpenClaw", @@ -15458,6 +15393,26 @@ "source": "Close canvas", "translated": "Tuvali kapat" }, + { + "id": "native.apple.a1db6ce0aed1d18e", + "source": "Chat, voice active", + "translated": "Sohbet, ses açık" + }, + { + "id": "native.apple.bda6cfa988e49a75", + "source": "Control", + "translated": "Kontrol" + }, + { + "id": "native.apple.3c80f5351055a5f3", + "source": "Agent", + "translated": "Aracı" + }, + { + "id": "native.apple.22d107db5b905ef4", + "source": "Settings", + "translated": "Ayarlar" + }, { "id": "native.apple.8a4751c86aa6fe77", "source": "Gateway needs attention", @@ -15488,11 +15443,6 @@ "source": "Chat", "translated": "Sohbet" }, - { - "id": "native.apple.febca7528af94342", - "source": "Talk", - "translated": "Konuş" - }, { "id": "native.apple.e26bbe303d8a9544", "source": "Overview", @@ -15863,6 +15813,11 @@ "source": "Requesting permissions…", "translated": "İzinler isteniyor…" }, + { + "id": "native.apple.198f59c97d7afe11", + "source": "Start failed: %@", + "translated": "Başlatılamadı: %@" + }, { "id": "native.apple.a1910b5c2e008b6c", "source": "Microphone permission denied", @@ -15873,11 +15828,6 @@ "source": "Speech recognition", "translated": "Konuşma tanıma" }, - { - "id": "native.apple.198f59c97d7afe11", - "source": "Start failed: %@", - "translated": "Başlatılamadı: %@" - }, { "id": "native.apple.c09f4e3bbef8177a", "source": "Offline", @@ -16088,66 +16038,6 @@ "source": "Listening (PTT)", "translated": "Dinleniyor (PTT)" }, - { - "id": "native.apple.0189269b6a863dc2", - "source": "Request ID", - "translated": "İstek kimliği" - }, - { - "id": "native.apple.f9d69f27f9d7f4de", - "source": "Sending...", - "translated": "Gönderiliyor..." - }, - { - "id": "native.apple.0c5336129f70faed", - "source": "Retry", - "translated": "Yeniden dene" - }, - { - "id": "native.apple.7b23f0a89d3ad411", - "source": "Sending approval request", - "translated": "Onay isteği gönderiliyor" - }, - { - "id": "native.apple.55db7317a2542e51", - "source": "Approval sent", - "translated": "Onay gönderildi" - }, - { - "id": "native.apple.06330f62ab254a0e", - "source": "Could not request approval", - "translated": "Onay istenemedi" - }, - { - "id": "native.apple.6580d3d1cdda682d", - "source": "Enable Talk", - "translated": "Talk’u etkinleştir" - }, - { - "id": "native.apple.abbfc1ba85a230b4", - "source": "Sending a new pairing request to your gateway...", - "translated": "Gateway’inize yeni bir eşleştirme isteği gönderiliyor..." - }, - { - "id": "native.apple.2880deb53ea2fd86", - "source": "Approve this request on your gateway. Talk will start automatically when approval lands.", - "translated": "Bu isteği gateway’inizde onaylayın. Onay geldiğinde Talk otomatik olarak başlayacak." - }, - { - "id": "native.apple.80faa829f543c03c", - "source": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider.", - "translated": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider." - }, - { - "id": "native.apple.88b0a2e2986fcebf", - "source": "Request Again", - "translated": "Tekrar iste" - }, - { - "id": "native.apple.5a3faae3472b4b86", - "source": "Send Approval Request", - "translated": "Onay isteği gönder" - }, { "id": "native.apple.1065cfe92d7c1eb6", "source": "Asking OpenClaw", @@ -22908,11 +22798,6 @@ "source": "Attachments", "translated": "Ekler" }, - { - "id": "native.apple.f8dda832ed76441d", - "source": "Voice note", - "translated": "Sesli not" - }, { "id": "native.apple.ee2c0d22f5861159", "source": "Talk", @@ -23898,6 +23783,11 @@ "source": "Dismiss", "translated": "Kapat" }, + { + "id": "native.apple.3892b7f55d9d1f0a", + "source": "Remove attachments or wait for delivery to resolve before starting a new chat.", + "translated": "Yeni bir sohbet başlatmadan önce ekleri kaldırın veya teslimatın tamamlanmasını bekleyin." + }, { "id": "native.apple.92fe9093903eda3b", "source": "Could not attach voice note: %@", @@ -23958,11 +23848,6 @@ "source": "Gateway changed before the session operation started.", "translated": "Oturum işlemi başlamadan önce Gateway değişti." }, - { - "id": "native.apple.d93e6fe16ec1b0e5", - "source": "Remove attachments or wait for delivery to resolve before starting a new chat.", - "translated": "Yeni bir sohbet başlatmadan önce ekleri kaldırın veya teslimatın tamamlanmasını bekleyin." - }, { "id": "native.apple.710c7bd117a7cc12", "source": "Remove attachments or wait for delivery to resolve before switching chats.", @@ -24188,6 +24073,61 @@ "source": "Compact Session", "translated": "Oturumu Sıkıştır" }, + { + "id": "native.apple.fa10740f9e272e83", + "source": "Voice note", + "translated": "Sesli not" + }, + { + "id": "native.apple.e4de5afc37981d6c", + "source": "Photo Library", + "translated": "Fotoğraf Arşivi" + }, + { + "id": "native.apple.37c19a66626b21fa", + "source": "Camera", + "translated": "Kamera" + }, + { + "id": "native.apple.333682684163ba6b", + "source": "Choose Image File", + "translated": "Görüntü Dosyası Seç" + }, + { + "id": "native.apple.534246792690968d", + "source": "Add attachment", + "translated": "Ek ekle" + }, + { + "id": "native.apple.e33210feaa3da016", + "source": "Record Voice Note", + "translated": "Sesli Not Kaydet" + }, + { + "id": "native.apple.00f0e15858e38d19", + "source": "Dictate message", + "translated": "Mesajı dikte et" + }, + { + "id": "native.apple.96f20175f61a2b29", + "source": "Listening", + "translated": "Dinleniyor" + }, + { + "id": "native.apple.eaa9047db6cfb67e", + "source": "Not listening", + "translated": "Dinlemiyor" + }, + { + "id": "native.apple.4185df7a940f09ef", + "source": "Finish dictation", + "translated": "Dikteyi bitir" + }, + { + "id": "native.apple.5a0793d2827b9399", + "source": "Transcribe speech into the message", + "translated": "Konuşmayı mesaja aktar." + }, { "id": "native.apple.f74d150ce05205dd", "source": "z", diff --git a/apps/.i18n/native/uk.json b/apps/.i18n/native/uk.json index 823e04b99086..2d8817edf08e 100644 --- a/apps/.i18n/native/uk.json +++ b/apps/.i18n/native/uk.json @@ -8358,11 +8358,21 @@ "source": "OpenClaw Activity", "translated": "Активність OpenClaw" }, + { + "id": "native.apple.093081590556db7f", + "source": "OPENCLAW", + "translated": "OPENCLAW" + }, { "id": "native.apple.86d546e1f1247028", "source": "OpenClaw", "translated": "OpenClaw" }, + { + "id": "native.apple.9fd01bdb5a753f3e", + "source": "LIVE", + "translated": "LIVE" + }, { "id": "native.apple.417ac9273b5f63ca", "source": "Connecting...", @@ -8383,6 +8393,36 @@ "source": "Action required", "translated": "Потрібна дія" }, + { + "id": "native.apple.275026f681242715", + "source": "Using %@", + "translated": "За допомогою %@" + }, + { + "id": "native.apple.360d0d0fc0df9eb2", + "source": "Using a tool", + "translated": "За допомогою інструмента" + }, + { + "id": "native.apple.0d0ee660962ea840", + "source": "Listening", + "translated": "Слухаю" + }, + { + "id": "native.apple.095ebf1496feb2ad", + "source": "Speaking", + "translated": "Озвучення" + }, + { + "id": "native.apple.63c98d2ca6a5b09c", + "source": "Voice active", + "translated": "Голос увімкнено" + }, + { + "id": "native.apple.02470f4b5348f97c", + "source": "Paused", + "translated": "Призупинені" + }, { "id": "native.apple.f2f3e6437fafcd1c", "source": "Connected", @@ -10518,11 +10558,36 @@ "source": "The session attaches once the gateway is ready.", "translated": "Сеанс під’єднається, щойно Gateway буде готовий." }, + { + "id": "native.apple.02611ddddef7295a", + "source": "Collapsed", + "translated": "Згорнуто" + }, + { + "id": "native.apple.1f4911f4a05230b5", + "source": "Expanded", + "translated": "Розгорнуто" + }, { "id": "native.apple.d19c9577e0fdfea7", "source": "Opens Settings / Gateway", "translated": "Відкриває Settings / Gateway" }, + { + "id": "native.apple.b59e690104ae3e24", + "source": "Hides the gateway status label", + "translated": "Приховує мітку стану шлюзу" + }, + { + "id": "native.apple.5fcf9d462463dd27", + "source": "Shows the full gateway status label", + "translated": "Показує повну мітку стану шлюзу" + }, + { + "id": "native.apple.592b1e673e42df50", + "source": "Voice off", + "translated": "Голос вимкнено" + }, { "id": "native.apple.7c87384c7d1370e1", "source": "New Chat", @@ -10548,6 +10613,11 @@ "source": "Export Transcript", "translated": "Експортувати транскрипт" }, + { + "id": "native.apple.d510e012c91b24ed", + "source": "Gateway Settings", + "translated": "Налаштування шлюзу" + }, { "id": "native.apple.f26cef22f417abb4", "source": "Show reasoning & tool activity", @@ -11728,11 +11798,6 @@ "source": "Agent chat and recent work.", "translated": "Чат агента та останні роботи." }, - { - "id": "native.apple.a30d0a950d0a77aa", - "source": "Realtime voice and controls.", - "translated": "Голос і керування в реальному часі." - }, { "id": "native.apple.a03e961ab14ba060", "source": "Overview", @@ -13818,151 +13883,6 @@ "source": "ClawHub did not report an installable version for this skill.", "translated": "ClawHub не повідомив про доступну для встановлення версію цієї навички." }, - { - "id": "native.apple.798c3584ad95f8da", - "source": "Enable Talk", - "translated": "Увімкнути розмову" - }, - { - "id": "native.apple.c6e171128190d131", - "source": "Not Now", - "translated": "Не зараз" - }, - { - "id": "native.apple.e4b15aef3c2e3da7", - "source": "Talk", - "translated": "Говорити" - }, - { - "id": "native.apple.ab3381742bdff650", - "source": "Conversation", - "translated": "Розмова" - }, - { - "id": "native.apple.7af100f6640c6a69", - "source": "Agent", - "translated": "Агент" - }, - { - "id": "native.apple.49c4de60117b1a98", - "source": "Session", - "translated": "Сеанс" - }, - { - "id": "native.apple.37c29d2506b955e9", - "source": "Runtime", - "translated": "Середовище виконання" - }, - { - "id": "native.apple.13e0ad79b141576e", - "source": "Voice Mode", - "translated": "Голосовий режим" - }, - { - "id": "native.apple.e8035b71bab833c7", - "source": "Configured", - "translated": "Налаштовано" - }, - { - "id": "native.apple.1b2845ec89d20eef", - "source": "Active", - "translated": "Активний" - }, - { - "id": "native.apple.a82c07df64d13b23", - "source": "Transport", - "translated": "Транспорт" - }, - { - "id": "native.apple.384e097278f76954", - "source": "Last issue", - "translated": "Остання проблема" - }, - { - "id": "native.apple.a6d8fc7bfd98836f", - "source": "Permission", - "translated": "Дозвіл" - }, - { - "id": "native.apple.983bdcd5e3f1baa8", - "source": "Speech language", - "translated": "Мова мовлення" - }, - { - "id": "native.apple.32e7fa2b1798e9c3", - "source": "Controls", - "translated": "Елементи керування" - }, - { - "id": "native.apple.0eb012a5d3d81b32", - "source": "Speakerphone", - "translated": "Гучний зв’язок" - }, - { - "id": "native.apple.ef8699e2718346da", - "source": "Background listening", - "translated": "Фонове прослуховування" - }, - { - "id": "native.apple.1e6938a09b4fbe77", - "source": "Voice & Talk Settings", - "translated": "Налаштування голосу й розмови" - }, - { - "id": "native.apple.c7a591fd5bb6687e", - "source": "Gateway approval is required before this phone can capture voice.", - "translated": "Перш ніж цей телефон зможе записувати голос, потрібне схвалення Gateway." - }, - { - "id": "native.apple.9d40e75e55ed39eb", - "source": "Voice is disabled in Apple Review demo mode.", - "translated": "Голос вимкнено в демонстраційному режимі Apple Review." - }, - { - "id": "native.apple.3ef69bf916177799", - "source": "Connect to your gateway to start a voice conversation.", - "translated": "Підключіться до свого шлюзу, щоб розпочати голосову розмову." - }, - { - "id": "native.apple.ed03af7c68841a5d", - "source": "Open Voice settings after the gateway loads Talk configuration.", - "translated": "Відкрийте налаштування голосу після того, як шлюз завантажить конфігурацію Talk." - }, - { - "id": "native.apple.883a251978601b96", - "source": "Routes voice to %@.", - "translated": "Спрямовує голос до %@." - }, - { - "id": "native.apple.877fb5c1abfaafa1", - "source": "Not loaded", - "translated": "Not loaded" - }, - { - "id": "native.apple.d3ccb092936551fc", - "source": "Not active", - "translated": "Неактивний" - }, - { - "id": "native.apple.1620f7cb3deea133", - "source": "Open Gateway Settings", - "translated": "Відкрити налаштування Gateway" - }, - { - "id": "native.apple.3fd2706ef0f14fd2", - "source": "Open Voice Settings", - "translated": "Відкрити налаштування голосу" - }, - { - "id": "native.apple.3cc9cd23a67db077", - "source": "Demo Mode Only", - "translated": "Лише деморежим" - }, - { - "id": "native.apple.12c9d8231777366c", - "source": "Waiting for Approval", - "translated": "Очікування схвалення" - }, { "id": "native.apple.dc57861a3ac5a08e", "source": "Open Settings", @@ -14528,6 +14448,41 @@ "source": "Action required", "translated": "Потрібна дія" }, + { + "id": "native.apple.7205f0822706cb55", + "source": "Listening", + "translated": "Слухаю" + }, + { + "id": "native.apple.8b10cb1dc21e42af", + "source": "Listening (PTT)", + "translated": "Прослуховування (PTT)" + }, + { + "id": "native.apple.939c7ec55636c67f", + "source": "Listening (Realtime)", + "translated": "Прослуховування (у реальному часі)" + }, + { + "id": "native.apple.86f203ebb2453d15", + "source": "Speaking…", + "translated": "Говорить…" + }, + { + "id": "native.apple.1e3ff6b56ae3828d", + "source": "Speaking (System)…", + "translated": "Озвучення (системне)…" + }, + { + "id": "native.apple.37a5fedaefb8ebf3", + "source": "Generating voice…", + "translated": "Створення голосу…" + }, + { + "id": "native.apple.47003f094346c660", + "source": "Ready", + "translated": "Готово" + }, { "id": "native.apple.cf0450b542893fb5", "source": "Off", @@ -15348,26 +15303,6 @@ "source": "unknown relay error", "translated": "unknown relay error" }, - { - "id": "native.apple.f2ea327bfea8b8e3", - "source": "Talk", - "translated": "Говорити" - }, - { - "id": "native.apple.bda6cfa988e49a75", - "source": "Control", - "translated": "Керування" - }, - { - "id": "native.apple.3c80f5351055a5f3", - "source": "Agent", - "translated": "Агент" - }, - { - "id": "native.apple.22d107db5b905ef4", - "source": "Settings", - "translated": "Налаштування" - }, { "id": "native.apple.c23d35de8d70863f", "source": "OpenClaw", @@ -15458,6 +15393,26 @@ "source": "Close canvas", "translated": "Закрити полотно" }, + { + "id": "native.apple.a1db6ce0aed1d18e", + "source": "Chat, voice active", + "translated": "Чат, голос увімкнено" + }, + { + "id": "native.apple.bda6cfa988e49a75", + "source": "Control", + "translated": "Керування" + }, + { + "id": "native.apple.3c80f5351055a5f3", + "source": "Agent", + "translated": "Агент" + }, + { + "id": "native.apple.22d107db5b905ef4", + "source": "Settings", + "translated": "Налаштування" + }, { "id": "native.apple.8a4751c86aa6fe77", "source": "Gateway needs attention", @@ -15488,11 +15443,6 @@ "source": "Chat", "translated": "Чат" }, - { - "id": "native.apple.febca7528af94342", - "source": "Talk", - "translated": "Розмова" - }, { "id": "native.apple.e26bbe303d8a9544", "source": "Overview", @@ -15863,6 +15813,11 @@ "source": "Requesting permissions…", "translated": "Запит дозволів…" }, + { + "id": "native.apple.198f59c97d7afe11", + "source": "Start failed: %@", + "translated": "Не вдалося запустити: %@" + }, { "id": "native.apple.a1910b5c2e008b6c", "source": "Microphone permission denied", @@ -15873,11 +15828,6 @@ "source": "Speech recognition", "translated": "Розпізнавання мовлення" }, - { - "id": "native.apple.198f59c97d7afe11", - "source": "Start failed: %@", - "translated": "Не вдалося запустити: %@" - }, { "id": "native.apple.c09f4e3bbef8177a", "source": "Offline", @@ -16088,66 +16038,6 @@ "source": "Listening (PTT)", "translated": "Прослуховування (PTT)" }, - { - "id": "native.apple.0189269b6a863dc2", - "source": "Request ID", - "translated": "ID запиту" - }, - { - "id": "native.apple.f9d69f27f9d7f4de", - "source": "Sending...", - "translated": "Надсилання..." - }, - { - "id": "native.apple.0c5336129f70faed", - "source": "Retry", - "translated": "Повторити" - }, - { - "id": "native.apple.7b23f0a89d3ad411", - "source": "Sending approval request", - "translated": "Надсилання запиту на схвалення" - }, - { - "id": "native.apple.55db7317a2542e51", - "source": "Approval sent", - "translated": "Схвалення надіслано" - }, - { - "id": "native.apple.06330f62ab254a0e", - "source": "Could not request approval", - "translated": "Не вдалося надіслати запит на схвалення" - }, - { - "id": "native.apple.6580d3d1cdda682d", - "source": "Enable Talk", - "translated": "Увімкнути Talk" - }, - { - "id": "native.apple.abbfc1ba85a230b4", - "source": "Sending a new pairing request to your gateway...", - "translated": "Надсилання нового запиту на сполучення до вашого Gateway..." - }, - { - "id": "native.apple.2880deb53ea2fd86", - "source": "Approve this request on your gateway. Talk will start automatically when approval lands.", - "translated": "Схваліть цей запит на вашому Gateway. Talk запуститься автоматично після отримання схвалення." - }, - { - "id": "native.apple.80faa829f543c03c", - "source": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider.", - "translated": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider." - }, - { - "id": "native.apple.88b0a2e2986fcebf", - "source": "Request Again", - "translated": "Запитати знову" - }, - { - "id": "native.apple.5a3faae3472b4b86", - "source": "Send Approval Request", - "translated": "Надіслати запит на схвалення" - }, { "id": "native.apple.1065cfe92d7c1eb6", "source": "Asking OpenClaw", @@ -22908,11 +22798,6 @@ "source": "Attachments", "translated": "Вкладення" }, - { - "id": "native.apple.f8dda832ed76441d", - "source": "Voice note", - "translated": "Голосове повідомлення" - }, { "id": "native.apple.ee2c0d22f5861159", "source": "Talk", @@ -23898,6 +23783,11 @@ "source": "Dismiss", "translated": "Закрити" }, + { + "id": "native.apple.3892b7f55d9d1f0a", + "source": "Remove attachments or wait for delivery to resolve before starting a new chat.", + "translated": "Видаліть вкладення або зачекайте на завершення доставлення, перш ніж починати новий чат." + }, { "id": "native.apple.92fe9093903eda3b", "source": "Could not attach voice note: %@", @@ -23958,11 +23848,6 @@ "source": "Gateway changed before the session operation started.", "translated": "Gateway змінився до початку операції із сеансом." }, - { - "id": "native.apple.d93e6fe16ec1b0e5", - "source": "Remove attachments or wait for delivery to resolve before starting a new chat.", - "translated": "Видаліть вкладення або зачекайте на завершення доставлення, перш ніж починати новий чат." - }, { "id": "native.apple.710c7bd117a7cc12", "source": "Remove attachments or wait for delivery to resolve before switching chats.", @@ -24188,6 +24073,61 @@ "source": "Compact Session", "translated": "Стиснути сесію" }, + { + "id": "native.apple.fa10740f9e272e83", + "source": "Voice note", + "translated": "Голосове повідомлення" + }, + { + "id": "native.apple.e4de5afc37981d6c", + "source": "Photo Library", + "translated": "Фототека" + }, + { + "id": "native.apple.37c19a66626b21fa", + "source": "Camera", + "translated": "Камера" + }, + { + "id": "native.apple.333682684163ba6b", + "source": "Choose Image File", + "translated": "Вибрати файл зображення" + }, + { + "id": "native.apple.534246792690968d", + "source": "Add attachment", + "translated": "Додати вкладення" + }, + { + "id": "native.apple.e33210feaa3da016", + "source": "Record Voice Note", + "translated": "Записати голосове повідомлення" + }, + { + "id": "native.apple.00f0e15858e38d19", + "source": "Dictate message", + "translated": "Продиктувати повідомлення" + }, + { + "id": "native.apple.96f20175f61a2b29", + "source": "Listening", + "translated": "Слухаю" + }, + { + "id": "native.apple.eaa9047db6cfb67e", + "source": "Not listening", + "translated": "Не слухає" + }, + { + "id": "native.apple.4185df7a940f09ef", + "source": "Finish dictation", + "translated": "Завершити диктування" + }, + { + "id": "native.apple.5a0793d2827b9399", + "source": "Transcribe speech into the message", + "translated": "Перетворити мовлення на текст повідомлення." + }, { "id": "native.apple.f74d150ce05205dd", "source": "z", diff --git a/apps/.i18n/native/vi.json b/apps/.i18n/native/vi.json index 6daf5508ec8e..a4cc6f54e084 100644 --- a/apps/.i18n/native/vi.json +++ b/apps/.i18n/native/vi.json @@ -8358,11 +8358,21 @@ "source": "OpenClaw Activity", "translated": "Hoạt động OpenClaw" }, + { + "id": "native.apple.093081590556db7f", + "source": "OPENCLAW", + "translated": "OPENCLAW" + }, { "id": "native.apple.86d546e1f1247028", "source": "OpenClaw", "translated": "OpenClaw" }, + { + "id": "native.apple.9fd01bdb5a753f3e", + "source": "LIVE", + "translated": "LIVE" + }, { "id": "native.apple.417ac9273b5f63ca", "source": "Connecting...", @@ -8383,6 +8393,36 @@ "source": "Action required", "translated": "Cần thực hiện hành động" }, + { + "id": "native.apple.275026f681242715", + "source": "Using %@", + "translated": "Đang dùng %@" + }, + { + "id": "native.apple.360d0d0fc0df9eb2", + "source": "Using a tool", + "translated": "Đang dùng công cụ" + }, + { + "id": "native.apple.0d0ee660962ea840", + "source": "Listening", + "translated": "Đang nghe" + }, + { + "id": "native.apple.095ebf1496feb2ad", + "source": "Speaking", + "translated": "Đang phát giọng nói" + }, + { + "id": "native.apple.63c98d2ca6a5b09c", + "source": "Voice active", + "translated": "Giọng nói đang bật" + }, + { + "id": "native.apple.02470f4b5348f97c", + "source": "Paused", + "translated": "Đã tạm dừng" + }, { "id": "native.apple.f2f3e6437fafcd1c", "source": "Connected", @@ -10518,11 +10558,36 @@ "source": "The session attaches once the gateway is ready.", "translated": "Phiên sẽ được đính kèm khi gateway sẵn sàng." }, + { + "id": "native.apple.02611ddddef7295a", + "source": "Collapsed", + "translated": "Thu gọn" + }, + { + "id": "native.apple.1f4911f4a05230b5", + "source": "Expanded", + "translated": "Mở rộng" + }, { "id": "native.apple.d19c9577e0fdfea7", "source": "Opens Settings / Gateway", "translated": "Mở Cài đặt / Gateway" }, + { + "id": "native.apple.b59e690104ae3e24", + "source": "Hides the gateway status label", + "translated": "Ẩn nhãn trạng thái cổng kết nối" + }, + { + "id": "native.apple.5fcf9d462463dd27", + "source": "Shows the full gateway status label", + "translated": "Hiện đầy đủ nhãn trạng thái cổng kết nối" + }, + { + "id": "native.apple.592b1e673e42df50", + "source": "Voice off", + "translated": "Giọng nói đang tắt" + }, { "id": "native.apple.7c87384c7d1370e1", "source": "New Chat", @@ -10548,6 +10613,11 @@ "source": "Export Transcript", "translated": "Xuất bản ghi cuộc trò chuyện" }, + { + "id": "native.apple.d510e012c91b24ed", + "source": "Gateway Settings", + "translated": "Cài đặt cổng kết nối" + }, { "id": "native.apple.f26cef22f417abb4", "source": "Show reasoning & tool activity", @@ -11728,11 +11798,6 @@ "source": "Agent chat and recent work.", "translated": "Trò chuyện với agent và công việc gần đây." }, - { - "id": "native.apple.a30d0a950d0a77aa", - "source": "Realtime voice and controls.", - "translated": "Giọng nói và điều khiển thời gian thực." - }, { "id": "native.apple.a03e961ab14ba060", "source": "Overview", @@ -13818,151 +13883,6 @@ "source": "ClawHub did not report an installable version for this skill.", "translated": "ClawHub không cung cấp phiên bản có thể cài đặt cho skill này." }, - { - "id": "native.apple.798c3584ad95f8da", - "source": "Enable Talk", - "translated": "Bật Talk" - }, - { - "id": "native.apple.c6e171128190d131", - "source": "Not Now", - "translated": "Không phải bây giờ" - }, - { - "id": "native.apple.e4b15aef3c2e3da7", - "source": "Talk", - "translated": "Nói chuyện" - }, - { - "id": "native.apple.ab3381742bdff650", - "source": "Conversation", - "translated": "Cuộc trò chuyện" - }, - { - "id": "native.apple.7af100f6640c6a69", - "source": "Agent", - "translated": "Tác nhân" - }, - { - "id": "native.apple.49c4de60117b1a98", - "source": "Session", - "translated": "Phiên" - }, - { - "id": "native.apple.37c29d2506b955e9", - "source": "Runtime", - "translated": "Thời gian chạy" - }, - { - "id": "native.apple.13e0ad79b141576e", - "source": "Voice Mode", - "translated": "Chế độ giọng nói" - }, - { - "id": "native.apple.e8035b71bab833c7", - "source": "Configured", - "translated": "Đã cấu hình" - }, - { - "id": "native.apple.1b2845ec89d20eef", - "source": "Active", - "translated": "Đang hoạt động" - }, - { - "id": "native.apple.a82c07df64d13b23", - "source": "Transport", - "translated": "Truyền tải" - }, - { - "id": "native.apple.384e097278f76954", - "source": "Last issue", - "translated": "Sự cố gần nhất" - }, - { - "id": "native.apple.a6d8fc7bfd98836f", - "source": "Permission", - "translated": "Quyền" - }, - { - "id": "native.apple.983bdcd5e3f1baa8", - "source": "Speech language", - "translated": "Ngôn ngữ nói" - }, - { - "id": "native.apple.32e7fa2b1798e9c3", - "source": "Controls", - "translated": "Điều khiển" - }, - { - "id": "native.apple.0eb012a5d3d81b32", - "source": "Speakerphone", - "translated": "Loa ngoài" - }, - { - "id": "native.apple.ef8699e2718346da", - "source": "Background listening", - "translated": "Nghe trong nền" - }, - { - "id": "native.apple.1e6938a09b4fbe77", - "source": "Voice & Talk Settings", - "translated": "Cài đặt Giọng nói & Trò chuyện" - }, - { - "id": "native.apple.c7a591fd5bb6687e", - "source": "Gateway approval is required before this phone can capture voice.", - "translated": "Cần có sự phê duyệt của Gateway trước khi điện thoại này có thể thu âm giọng nói." - }, - { - "id": "native.apple.9d40e75e55ed39eb", - "source": "Voice is disabled in Apple Review demo mode.", - "translated": "Tính năng giọng nói bị tắt trong chế độ demo Apple Review." - }, - { - "id": "native.apple.3ef69bf916177799", - "source": "Connect to your gateway to start a voice conversation.", - "translated": "Kết nối với Gateway của bạn để bắt đầu cuộc trò chuyện bằng giọng nói." - }, - { - "id": "native.apple.ed03af7c68841a5d", - "source": "Open Voice settings after the gateway loads Talk configuration.", - "translated": "Mở phần cài đặt Giọng nói sau khi Gateway tải cấu hình Talk." - }, - { - "id": "native.apple.883a251978601b96", - "source": "Routes voice to %@.", - "translated": "Định tuyến giọng nói đến %@." - }, - { - "id": "native.apple.877fb5c1abfaafa1", - "source": "Not loaded", - "translated": "Not loaded" - }, - { - "id": "native.apple.d3ccb092936551fc", - "source": "Not active", - "translated": "Không hoạt động" - }, - { - "id": "native.apple.1620f7cb3deea133", - "source": "Open Gateway Settings", - "translated": "Mở cài đặt Gateway" - }, - { - "id": "native.apple.3fd2706ef0f14fd2", - "source": "Open Voice Settings", - "translated": "Mở cài đặt giọng nói" - }, - { - "id": "native.apple.3cc9cd23a67db077", - "source": "Demo Mode Only", - "translated": "Chỉ ở chế độ demo" - }, - { - "id": "native.apple.12c9d8231777366c", - "source": "Waiting for Approval", - "translated": "Đang chờ phê duyệt" - }, { "id": "native.apple.dc57861a3ac5a08e", "source": "Open Settings", @@ -14528,6 +14448,41 @@ "source": "Action required", "translated": "Cần thực hiện hành động" }, + { + "id": "native.apple.7205f0822706cb55", + "source": "Listening", + "translated": "Đang nghe" + }, + { + "id": "native.apple.8b10cb1dc21e42af", + "source": "Listening (PTT)", + "translated": "Đang nghe (PTT)" + }, + { + "id": "native.apple.939c7ec55636c67f", + "source": "Listening (Realtime)", + "translated": "Đang nghe (Thời gian thực)" + }, + { + "id": "native.apple.86f203ebb2453d15", + "source": "Speaking…", + "translated": "Đang nói…" + }, + { + "id": "native.apple.1e3ff6b56ae3828d", + "source": "Speaking (System)…", + "translated": "Đang phát giọng nói (Hệ thống)…" + }, + { + "id": "native.apple.37a5fedaefb8ebf3", + "source": "Generating voice…", + "translated": "Đang tạo giọng nói…" + }, + { + "id": "native.apple.47003f094346c660", + "source": "Ready", + "translated": "Sẵn sàng" + }, { "id": "native.apple.cf0450b542893fb5", "source": "Off", @@ -15348,26 +15303,6 @@ "source": "unknown relay error", "translated": "unknown relay error" }, - { - "id": "native.apple.f2ea327bfea8b8e3", - "source": "Talk", - "translated": "Trò chuyện" - }, - { - "id": "native.apple.bda6cfa988e49a75", - "source": "Control", - "translated": "Điều khiển" - }, - { - "id": "native.apple.3c80f5351055a5f3", - "source": "Agent", - "translated": "Tác nhân" - }, - { - "id": "native.apple.22d107db5b905ef4", - "source": "Settings", - "translated": "Cài đặt" - }, { "id": "native.apple.c23d35de8d70863f", "source": "OpenClaw", @@ -15458,6 +15393,26 @@ "source": "Close canvas", "translated": "Đóng canvas" }, + { + "id": "native.apple.a1db6ce0aed1d18e", + "source": "Chat, voice active", + "translated": "Chat, giọng nói đang bật" + }, + { + "id": "native.apple.bda6cfa988e49a75", + "source": "Control", + "translated": "Điều khiển" + }, + { + "id": "native.apple.3c80f5351055a5f3", + "source": "Agent", + "translated": "Tác nhân" + }, + { + "id": "native.apple.22d107db5b905ef4", + "source": "Settings", + "translated": "Cài đặt" + }, { "id": "native.apple.8a4751c86aa6fe77", "source": "Gateway needs attention", @@ -15488,11 +15443,6 @@ "source": "Chat", "translated": "Trò chuyện" }, - { - "id": "native.apple.febca7528af94342", - "source": "Talk", - "translated": "Nói chuyện" - }, { "id": "native.apple.e26bbe303d8a9544", "source": "Overview", @@ -15863,6 +15813,11 @@ "source": "Requesting permissions…", "translated": "Đang yêu cầu quyền…" }, + { + "id": "native.apple.198f59c97d7afe11", + "source": "Start failed: %@", + "translated": "Không thể khởi động: %@" + }, { "id": "native.apple.a1910b5c2e008b6c", "source": "Microphone permission denied", @@ -15873,11 +15828,6 @@ "source": "Speech recognition", "translated": "Nhận dạng giọng nói" }, - { - "id": "native.apple.198f59c97d7afe11", - "source": "Start failed: %@", - "translated": "Không thể khởi động: %@" - }, { "id": "native.apple.c09f4e3bbef8177a", "source": "Offline", @@ -16088,66 +16038,6 @@ "source": "Listening (PTT)", "translated": "Đang nghe (PTT)" }, - { - "id": "native.apple.0189269b6a863dc2", - "source": "Request ID", - "translated": "ID yêu cầu" - }, - { - "id": "native.apple.f9d69f27f9d7f4de", - "source": "Sending...", - "translated": "Đang gửi..." - }, - { - "id": "native.apple.0c5336129f70faed", - "source": "Retry", - "translated": "Thử lại" - }, - { - "id": "native.apple.7b23f0a89d3ad411", - "source": "Sending approval request", - "translated": "Đang gửi yêu cầu phê duyệt" - }, - { - "id": "native.apple.55db7317a2542e51", - "source": "Approval sent", - "translated": "Đã gửi phê duyệt" - }, - { - "id": "native.apple.06330f62ab254a0e", - "source": "Could not request approval", - "translated": "Không thể yêu cầu phê duyệt" - }, - { - "id": "native.apple.6580d3d1cdda682d", - "source": "Enable Talk", - "translated": "Bật Talk" - }, - { - "id": "native.apple.abbfc1ba85a230b4", - "source": "Sending a new pairing request to your gateway...", - "translated": "Đang gửi yêu cầu ghép đôi mới đến gateway của bạn..." - }, - { - "id": "native.apple.2880deb53ea2fd86", - "source": "Approve this request on your gateway. Talk will start automatically when approval lands.", - "translated": "Phê duyệt yêu cầu này trên gateway của bạn. Talk sẽ tự động bắt đầu khi phê duyệt đến." - }, - { - "id": "native.apple.80faa829f543c03c", - "source": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider.", - "translated": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider." - }, - { - "id": "native.apple.88b0a2e2986fcebf", - "source": "Request Again", - "translated": "Yêu cầu lại" - }, - { - "id": "native.apple.5a3faae3472b4b86", - "source": "Send Approval Request", - "translated": "Gửi yêu cầu phê duyệt" - }, { "id": "native.apple.1065cfe92d7c1eb6", "source": "Asking OpenClaw", @@ -22908,11 +22798,6 @@ "source": "Attachments", "translated": "Tệp đính kèm" }, - { - "id": "native.apple.f8dda832ed76441d", - "source": "Voice note", - "translated": "Ghi chú thoại" - }, { "id": "native.apple.ee2c0d22f5861159", "source": "Talk", @@ -23898,6 +23783,11 @@ "source": "Dismiss", "translated": "Đóng" }, + { + "id": "native.apple.3892b7f55d9d1f0a", + "source": "Remove attachments or wait for delivery to resolve before starting a new chat.", + "translated": "Gỡ tệp đính kèm hoặc chờ gửi xong trước khi bắt đầu cuộc trò chuyện mới." + }, { "id": "native.apple.92fe9093903eda3b", "source": "Could not attach voice note: %@", @@ -23958,11 +23848,6 @@ "source": "Gateway changed before the session operation started.", "translated": "Gateway đã thay đổi trước khi thao tác phiên bắt đầu." }, - { - "id": "native.apple.d93e6fe16ec1b0e5", - "source": "Remove attachments or wait for delivery to resolve before starting a new chat.", - "translated": "Gỡ tệp đính kèm hoặc chờ gửi xong trước khi bắt đầu cuộc trò chuyện mới." - }, { "id": "native.apple.710c7bd117a7cc12", "source": "Remove attachments or wait for delivery to resolve before switching chats.", @@ -24188,6 +24073,61 @@ "source": "Compact Session", "translated": "Thu gọn Phiên" }, + { + "id": "native.apple.fa10740f9e272e83", + "source": "Voice note", + "translated": "Ghi chú thoại" + }, + { + "id": "native.apple.e4de5afc37981d6c", + "source": "Photo Library", + "translated": "Thư viện ảnh" + }, + { + "id": "native.apple.37c19a66626b21fa", + "source": "Camera", + "translated": "Máy ảnh" + }, + { + "id": "native.apple.333682684163ba6b", + "source": "Choose Image File", + "translated": "Chọn tệp hình ảnh" + }, + { + "id": "native.apple.534246792690968d", + "source": "Add attachment", + "translated": "Thêm tệp đính kèm" + }, + { + "id": "native.apple.e33210feaa3da016", + "source": "Record Voice Note", + "translated": "Ghi âm ghi chú giọng nói" + }, + { + "id": "native.apple.00f0e15858e38d19", + "source": "Dictate message", + "translated": "Đọc để nhập tin nhắn" + }, + { + "id": "native.apple.96f20175f61a2b29", + "source": "Listening", + "translated": "Đang nghe" + }, + { + "id": "native.apple.eaa9047db6cfb67e", + "source": "Not listening", + "translated": "Không nghe" + }, + { + "id": "native.apple.4185df7a940f09ef", + "source": "Finish dictation", + "translated": "Kết thúc đọc" + }, + { + "id": "native.apple.5a0793d2827b9399", + "source": "Transcribe speech into the message", + "translated": "Chuyển lời nói thành văn bản trong tin nhắn." + }, { "id": "native.apple.f74d150ce05205dd", "source": "z", diff --git a/apps/.i18n/native/zh-CN.json b/apps/.i18n/native/zh-CN.json index 98ffc0977b16..606697ca3bb8 100644 --- a/apps/.i18n/native/zh-CN.json +++ b/apps/.i18n/native/zh-CN.json @@ -8358,11 +8358,21 @@ "source": "OpenClaw Activity", "translated": "OpenClaw 活动" }, + { + "id": "native.apple.093081590556db7f", + "source": "OPENCLAW", + "translated": "OPENCLAW" + }, { "id": "native.apple.86d546e1f1247028", "source": "OpenClaw", "translated": "OpenClaw" }, + { + "id": "native.apple.9fd01bdb5a753f3e", + "source": "LIVE", + "translated": "LIVE" + }, { "id": "native.apple.417ac9273b5f63ca", "source": "Connecting...", @@ -8383,6 +8393,36 @@ "source": "Action required", "translated": "需要操作" }, + { + "id": "native.apple.275026f681242715", + "source": "Using %@", + "translated": "使用 %@" + }, + { + "id": "native.apple.360d0d0fc0df9eb2", + "source": "Using a tool", + "translated": "使用工具" + }, + { + "id": "native.apple.0d0ee660962ea840", + "source": "Listening", + "translated": "正在聆听" + }, + { + "id": "native.apple.095ebf1496feb2ad", + "source": "Speaking", + "translated": "正在朗读" + }, + { + "id": "native.apple.63c98d2ca6a5b09c", + "source": "Voice active", + "translated": "语音已开启" + }, + { + "id": "native.apple.02470f4b5348f97c", + "source": "Paused", + "translated": "已暂停" + }, { "id": "native.apple.f2f3e6437fafcd1c", "source": "Connected", @@ -10518,11 +10558,36 @@ "source": "The session attaches once the gateway is ready.", "translated": "Gateway 准备就绪后,会话将会附加。" }, + { + "id": "native.apple.02611ddddef7295a", + "source": "Collapsed", + "translated": "已收起" + }, + { + "id": "native.apple.1f4911f4a05230b5", + "source": "Expanded", + "translated": "已展开" + }, { "id": "native.apple.d19c9577e0fdfea7", "source": "Opens Settings / Gateway", "translated": "打开“设置”/“Gateway”" }, + { + "id": "native.apple.b59e690104ae3e24", + "source": "Hides the gateway status label", + "translated": "隐藏网关状态标签" + }, + { + "id": "native.apple.5fcf9d462463dd27", + "source": "Shows the full gateway status label", + "translated": "显示完整网关状态标签" + }, + { + "id": "native.apple.592b1e673e42df50", + "source": "Voice off", + "translated": "语音已关闭" + }, { "id": "native.apple.7c87384c7d1370e1", "source": "New Chat", @@ -10548,6 +10613,11 @@ "source": "Export Transcript", "translated": "导出转录" }, + { + "id": "native.apple.d510e012c91b24ed", + "source": "Gateway Settings", + "translated": "网关设置" + }, { "id": "native.apple.f26cef22f417abb4", "source": "Show reasoning & tool activity", @@ -11728,11 +11798,6 @@ "source": "Agent chat and recent work.", "translated": "智能体聊天和近期工作。" }, - { - "id": "native.apple.a30d0a950d0a77aa", - "source": "Realtime voice and controls.", - "translated": "实时语音和控制。" - }, { "id": "native.apple.a03e961ab14ba060", "source": "Overview", @@ -13818,151 +13883,6 @@ "source": "ClawHub did not report an installable version for this skill.", "translated": "ClawHub 未提供此技能的可安装版本。" }, - { - "id": "native.apple.798c3584ad95f8da", - "source": "Enable Talk", - "translated": "启用对话" - }, - { - "id": "native.apple.c6e171128190d131", - "source": "Not Now", - "translated": "暂不" - }, - { - "id": "native.apple.e4b15aef3c2e3da7", - "source": "Talk", - "translated": "通话" - }, - { - "id": "native.apple.ab3381742bdff650", - "source": "Conversation", - "translated": "对话" - }, - { - "id": "native.apple.7af100f6640c6a69", - "source": "Agent", - "translated": "智能体" - }, - { - "id": "native.apple.49c4de60117b1a98", - "source": "Session", - "translated": "会话" - }, - { - "id": "native.apple.37c29d2506b955e9", - "source": "Runtime", - "translated": "运行时" - }, - { - "id": "native.apple.13e0ad79b141576e", - "source": "Voice Mode", - "translated": "语音模式" - }, - { - "id": "native.apple.e8035b71bab833c7", - "source": "Configured", - "translated": "已配置" - }, - { - "id": "native.apple.1b2845ec89d20eef", - "source": "Active", - "translated": "活动" - }, - { - "id": "native.apple.a82c07df64d13b23", - "source": "Transport", - "translated": "传输" - }, - { - "id": "native.apple.384e097278f76954", - "source": "Last issue", - "translated": "上次问题" - }, - { - "id": "native.apple.a6d8fc7bfd98836f", - "source": "Permission", - "translated": "权限" - }, - { - "id": "native.apple.983bdcd5e3f1baa8", - "source": "Speech language", - "translated": "语音语言" - }, - { - "id": "native.apple.32e7fa2b1798e9c3", - "source": "Controls", - "translated": "控制" - }, - { - "id": "native.apple.0eb012a5d3d81b32", - "source": "Speakerphone", - "translated": "扬声器" - }, - { - "id": "native.apple.ef8699e2718346da", - "source": "Background listening", - "translated": "后台监听" - }, - { - "id": "native.apple.1e6938a09b4fbe77", - "source": "Voice & Talk Settings", - "translated": "语音和通话设置" - }, - { - "id": "native.apple.c7a591fd5bb6687e", - "source": "Gateway approval is required before this phone can capture voice.", - "translated": "此手机需要获得 Gateway 批准后才能采集语音。" - }, - { - "id": "native.apple.9d40e75e55ed39eb", - "source": "Voice is disabled in Apple Review demo mode.", - "translated": "Apple 审核演示模式下已禁用语音。" - }, - { - "id": "native.apple.3ef69bf916177799", - "source": "Connect to your gateway to start a voice conversation.", - "translated": "连接到 Gateway 以开始语音对话。" - }, - { - "id": "native.apple.ed03af7c68841a5d", - "source": "Open Voice settings after the gateway loads Talk configuration.", - "translated": "Gateway 加载 Talk 配置后,打开语音设置。" - }, - { - "id": "native.apple.883a251978601b96", - "source": "Routes voice to %@.", - "translated": "将语音路由到 %@。" - }, - { - "id": "native.apple.877fb5c1abfaafa1", - "source": "Not loaded", - "translated": "Not loaded" - }, - { - "id": "native.apple.d3ccb092936551fc", - "source": "Not active", - "translated": "未启用" - }, - { - "id": "native.apple.1620f7cb3deea133", - "source": "Open Gateway Settings", - "translated": "打开 Gateway 设置" - }, - { - "id": "native.apple.3fd2706ef0f14fd2", - "source": "Open Voice Settings", - "translated": "打开语音设置" - }, - { - "id": "native.apple.3cc9cd23a67db077", - "source": "Demo Mode Only", - "translated": "仅演示模式" - }, - { - "id": "native.apple.12c9d8231777366c", - "source": "Waiting for Approval", - "translated": "等待批准" - }, { "id": "native.apple.dc57861a3ac5a08e", "source": "Open Settings", @@ -14528,6 +14448,41 @@ "source": "Action required", "translated": "需要操作" }, + { + "id": "native.apple.7205f0822706cb55", + "source": "Listening", + "translated": "正在聆听" + }, + { + "id": "native.apple.8b10cb1dc21e42af", + "source": "Listening (PTT)", + "translated": "正在聆听(按住说话)" + }, + { + "id": "native.apple.939c7ec55636c67f", + "source": "Listening (Realtime)", + "translated": "正在聆听(实时)" + }, + { + "id": "native.apple.86f203ebb2453d15", + "source": "Speaking…", + "translated": "正在朗读…" + }, + { + "id": "native.apple.1e3ff6b56ae3828d", + "source": "Speaking (System)…", + "translated": "正在朗读(系统)…" + }, + { + "id": "native.apple.37a5fedaefb8ebf3", + "source": "Generating voice…", + "translated": "正在生成语音…" + }, + { + "id": "native.apple.47003f094346c660", + "source": "Ready", + "translated": "就绪" + }, { "id": "native.apple.cf0450b542893fb5", "source": "Off", @@ -15348,26 +15303,6 @@ "source": "unknown relay error", "translated": "unknown relay error" }, - { - "id": "native.apple.f2ea327bfea8b8e3", - "source": "Talk", - "translated": "对话" - }, - { - "id": "native.apple.bda6cfa988e49a75", - "source": "Control", - "translated": "控制" - }, - { - "id": "native.apple.3c80f5351055a5f3", - "source": "Agent", - "translated": "代理" - }, - { - "id": "native.apple.22d107db5b905ef4", - "source": "Settings", - "translated": "设置" - }, { "id": "native.apple.c23d35de8d70863f", "source": "OpenClaw", @@ -15458,6 +15393,26 @@ "source": "Close canvas", "translated": "关闭画布" }, + { + "id": "native.apple.a1db6ce0aed1d18e", + "source": "Chat, voice active", + "translated": "聊天,语音已开启" + }, + { + "id": "native.apple.bda6cfa988e49a75", + "source": "Control", + "translated": "控制" + }, + { + "id": "native.apple.3c80f5351055a5f3", + "source": "Agent", + "translated": "代理" + }, + { + "id": "native.apple.22d107db5b905ef4", + "source": "Settings", + "translated": "设置" + }, { "id": "native.apple.8a4751c86aa6fe77", "source": "Gateway needs attention", @@ -15488,11 +15443,6 @@ "source": "Chat", "translated": "聊天" }, - { - "id": "native.apple.febca7528af94342", - "source": "Talk", - "translated": "对话" - }, { "id": "native.apple.e26bbe303d8a9544", "source": "Overview", @@ -15863,6 +15813,11 @@ "source": "Requesting permissions…", "translated": "正在请求权限…" }, + { + "id": "native.apple.198f59c97d7afe11", + "source": "Start failed: %@", + "translated": "启动失败:%@" + }, { "id": "native.apple.a1910b5c2e008b6c", "source": "Microphone permission denied", @@ -15873,11 +15828,6 @@ "source": "Speech recognition", "translated": "语音识别" }, - { - "id": "native.apple.198f59c97d7afe11", - "source": "Start failed: %@", - "translated": "启动失败:%@" - }, { "id": "native.apple.c09f4e3bbef8177a", "source": "Offline", @@ -16088,66 +16038,6 @@ "source": "Listening (PTT)", "translated": "正在聆听(按住说话)" }, - { - "id": "native.apple.0189269b6a863dc2", - "source": "Request ID", - "translated": "请求 ID" - }, - { - "id": "native.apple.f9d69f27f9d7f4de", - "source": "Sending...", - "translated": "正在发送..." - }, - { - "id": "native.apple.0c5336129f70faed", - "source": "Retry", - "translated": "重试" - }, - { - "id": "native.apple.7b23f0a89d3ad411", - "source": "Sending approval request", - "translated": "正在发送审批请求" - }, - { - "id": "native.apple.55db7317a2542e51", - "source": "Approval sent", - "translated": "审批已发送" - }, - { - "id": "native.apple.06330f62ab254a0e", - "source": "Could not request approval", - "translated": "无法请求审批" - }, - { - "id": "native.apple.6580d3d1cdda682d", - "source": "Enable Talk", - "translated": "启用 Talk" - }, - { - "id": "native.apple.abbfc1ba85a230b4", - "source": "Sending a new pairing request to your gateway...", - "translated": "正在向您的 gateway 发送新的配对请求..." - }, - { - "id": "native.apple.2880deb53ea2fd86", - "source": "Approve this request on your gateway. Talk will start automatically when approval lands.", - "translated": "请在您的 gateway 上批准此请求。审批完成后,Talk 将自动启动。" - }, - { - "id": "native.apple.80faa829f543c03c", - "source": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider.", - "translated": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider." - }, - { - "id": "native.apple.88b0a2e2986fcebf", - "source": "Request Again", - "translated": "再次请求" - }, - { - "id": "native.apple.5a3faae3472b4b86", - "source": "Send Approval Request", - "translated": "发送审批请求" - }, { "id": "native.apple.1065cfe92d7c1eb6", "source": "Asking OpenClaw", @@ -22908,11 +22798,6 @@ "source": "Attachments", "translated": "附件" }, - { - "id": "native.apple.f8dda832ed76441d", - "source": "Voice note", - "translated": "语音消息" - }, { "id": "native.apple.ee2c0d22f5861159", "source": "Talk", @@ -23898,6 +23783,11 @@ "source": "Dismiss", "translated": "关闭" }, + { + "id": "native.apple.3892b7f55d9d1f0a", + "source": "Remove attachments or wait for delivery to resolve before starting a new chat.", + "translated": "请移除附件或等待发送完成后再开始新聊天。" + }, { "id": "native.apple.92fe9093903eda3b", "source": "Could not attach voice note: %@", @@ -23958,11 +23848,6 @@ "source": "Gateway changed before the session operation started.", "translated": "Gateway 在会话操作开始前已更改。" }, - { - "id": "native.apple.d93e6fe16ec1b0e5", - "source": "Remove attachments or wait for delivery to resolve before starting a new chat.", - "translated": "请移除附件或等待发送完成后再开始新聊天。" - }, { "id": "native.apple.710c7bd117a7cc12", "source": "Remove attachments or wait for delivery to resolve before switching chats.", @@ -24188,6 +24073,61 @@ "source": "Compact Session", "translated": "压缩会话" }, + { + "id": "native.apple.fa10740f9e272e83", + "source": "Voice note", + "translated": "语音消息" + }, + { + "id": "native.apple.e4de5afc37981d6c", + "source": "Photo Library", + "translated": "照片图库" + }, + { + "id": "native.apple.37c19a66626b21fa", + "source": "Camera", + "translated": "相机" + }, + { + "id": "native.apple.333682684163ba6b", + "source": "Choose Image File", + "translated": "选择图片文件" + }, + { + "id": "native.apple.534246792690968d", + "source": "Add attachment", + "translated": "添加附件" + }, + { + "id": "native.apple.e33210feaa3da016", + "source": "Record Voice Note", + "translated": "录制语音留言" + }, + { + "id": "native.apple.00f0e15858e38d19", + "source": "Dictate message", + "translated": "语音输入消息" + }, + { + "id": "native.apple.96f20175f61a2b29", + "source": "Listening", + "translated": "正在聆听" + }, + { + "id": "native.apple.eaa9047db6cfb67e", + "source": "Not listening", + "translated": "未在收听" + }, + { + "id": "native.apple.4185df7a940f09ef", + "source": "Finish dictation", + "translated": "结束语音输入" + }, + { + "id": "native.apple.5a0793d2827b9399", + "source": "Transcribe speech into the message", + "translated": "将语音转写到消息中。" + }, { "id": "native.apple.f74d150ce05205dd", "source": "z", diff --git a/apps/.i18n/native/zh-TW.json b/apps/.i18n/native/zh-TW.json index 7fbd4cd3c713..acc67be7b833 100644 --- a/apps/.i18n/native/zh-TW.json +++ b/apps/.i18n/native/zh-TW.json @@ -8358,11 +8358,21 @@ "source": "OpenClaw Activity", "translated": "OpenClaw 活動" }, + { + "id": "native.apple.093081590556db7f", + "source": "OPENCLAW", + "translated": "OPENCLAW" + }, { "id": "native.apple.86d546e1f1247028", "source": "OpenClaw", "translated": "OpenClaw" }, + { + "id": "native.apple.9fd01bdb5a753f3e", + "source": "LIVE", + "translated": "LIVE" + }, { "id": "native.apple.417ac9273b5f63ca", "source": "Connecting...", @@ -8383,6 +8393,36 @@ "source": "Action required", "translated": "需要採取動作" }, + { + "id": "native.apple.275026f681242715", + "source": "Using %@", + "translated": "使用 %@" + }, + { + "id": "native.apple.360d0d0fc0df9eb2", + "source": "Using a tool", + "translated": "使用工具" + }, + { + "id": "native.apple.0d0ee660962ea840", + "source": "Listening", + "translated": "正在聆聽" + }, + { + "id": "native.apple.095ebf1496feb2ad", + "source": "Speaking", + "translated": "正在朗讀" + }, + { + "id": "native.apple.63c98d2ca6a5b09c", + "source": "Voice active", + "translated": "語音已開啟" + }, + { + "id": "native.apple.02470f4b5348f97c", + "source": "Paused", + "translated": "已暫停" + }, { "id": "native.apple.f2f3e6437fafcd1c", "source": "Connected", @@ -10518,11 +10558,36 @@ "source": "The session attaches once the gateway is ready.", "translated": "Gateway 準備就緒後,工作階段會連接。" }, + { + "id": "native.apple.02611ddddef7295a", + "source": "Collapsed", + "translated": "已收合" + }, + { + "id": "native.apple.1f4911f4a05230b5", + "source": "Expanded", + "translated": "已展開" + }, { "id": "native.apple.d19c9577e0fdfea7", "source": "Opens Settings / Gateway", "translated": "開啟「設定」/ Gateway" }, + { + "id": "native.apple.b59e690104ae3e24", + "source": "Hides the gateway status label", + "translated": "隱藏閘道狀態標籤" + }, + { + "id": "native.apple.5fcf9d462463dd27", + "source": "Shows the full gateway status label", + "translated": "顯示完整閘道狀態標籤" + }, + { + "id": "native.apple.592b1e673e42df50", + "source": "Voice off", + "translated": "語音已關閉" + }, { "id": "native.apple.7c87384c7d1370e1", "source": "New Chat", @@ -10548,6 +10613,11 @@ "source": "Export Transcript", "translated": "匯出逐字稿" }, + { + "id": "native.apple.d510e012c91b24ed", + "source": "Gateway Settings", + "translated": "閘道設定" + }, { "id": "native.apple.f26cef22f417abb4", "source": "Show reasoning & tool activity", @@ -11728,11 +11798,6 @@ "source": "Agent chat and recent work.", "translated": "代理聊天與近期工作。" }, - { - "id": "native.apple.a30d0a950d0a77aa", - "source": "Realtime voice and controls.", - "translated": "即時語音與控制。" - }, { "id": "native.apple.a03e961ab14ba060", "source": "Overview", @@ -13818,151 +13883,6 @@ "source": "ClawHub did not report an installable version for this skill.", "translated": "ClawHub 未提供此 skill 的可安裝版本。" }, - { - "id": "native.apple.798c3584ad95f8da", - "source": "Enable Talk", - "translated": "啟用對話" - }, - { - "id": "native.apple.c6e171128190d131", - "source": "Not Now", - "translated": "暫時不要" - }, - { - "id": "native.apple.e4b15aef3c2e3da7", - "source": "Talk", - "translated": "對話" - }, - { - "id": "native.apple.ab3381742bdff650", - "source": "Conversation", - "translated": "對話" - }, - { - "id": "native.apple.7af100f6640c6a69", - "source": "Agent", - "translated": "代理程式" - }, - { - "id": "native.apple.49c4de60117b1a98", - "source": "Session", - "translated": "工作階段" - }, - { - "id": "native.apple.37c29d2506b955e9", - "source": "Runtime", - "translated": "執行階段" - }, - { - "id": "native.apple.13e0ad79b141576e", - "source": "Voice Mode", - "translated": "語音模式" - }, - { - "id": "native.apple.e8035b71bab833c7", - "source": "Configured", - "translated": "已設定" - }, - { - "id": "native.apple.1b2845ec89d20eef", - "source": "Active", - "translated": "使用中" - }, - { - "id": "native.apple.a82c07df64d13b23", - "source": "Transport", - "translated": "傳輸" - }, - { - "id": "native.apple.384e097278f76954", - "source": "Last issue", - "translated": "上次問題" - }, - { - "id": "native.apple.a6d8fc7bfd98836f", - "source": "Permission", - "translated": "權限" - }, - { - "id": "native.apple.983bdcd5e3f1baa8", - "source": "Speech language", - "translated": "語音語言" - }, - { - "id": "native.apple.32e7fa2b1798e9c3", - "source": "Controls", - "translated": "控制項" - }, - { - "id": "native.apple.0eb012a5d3d81b32", - "source": "Speakerphone", - "translated": "免持聽筒" - }, - { - "id": "native.apple.ef8699e2718346da", - "source": "Background listening", - "translated": "背景聆聽" - }, - { - "id": "native.apple.1e6938a09b4fbe77", - "source": "Voice & Talk Settings", - "translated": "語音與通話設定" - }, - { - "id": "native.apple.c7a591fd5bb6687e", - "source": "Gateway approval is required before this phone can capture voice.", - "translated": "此手機必須先獲得 Gateway 核准,才能擷取語音。" - }, - { - "id": "native.apple.9d40e75e55ed39eb", - "source": "Voice is disabled in Apple Review demo mode.", - "translated": "Apple 審查展示模式已停用語音功能。" - }, - { - "id": "native.apple.3ef69bf916177799", - "source": "Connect to your gateway to start a voice conversation.", - "translated": "連線至 Gateway 以開始語音對話。" - }, - { - "id": "native.apple.ed03af7c68841a5d", - "source": "Open Voice settings after the gateway loads Talk configuration.", - "translated": "Gateway 載入 Talk 設定後,開啟語音設定。" - }, - { - "id": "native.apple.883a251978601b96", - "source": "Routes voice to %@.", - "translated": "將語音導向 %@。" - }, - { - "id": "native.apple.877fb5c1abfaafa1", - "source": "Not loaded", - "translated": "Not loaded" - }, - { - "id": "native.apple.d3ccb092936551fc", - "source": "Not active", - "translated": "未啟用" - }, - { - "id": "native.apple.1620f7cb3deea133", - "source": "Open Gateway Settings", - "translated": "開啟 Gateway 設定" - }, - { - "id": "native.apple.3fd2706ef0f14fd2", - "source": "Open Voice Settings", - "translated": "開啟語音設定" - }, - { - "id": "native.apple.3cc9cd23a67db077", - "source": "Demo Mode Only", - "translated": "僅限示範模式" - }, - { - "id": "native.apple.12c9d8231777366c", - "source": "Waiting for Approval", - "translated": "等待核准" - }, { "id": "native.apple.dc57861a3ac5a08e", "source": "Open Settings", @@ -14528,6 +14448,41 @@ "source": "Action required", "translated": "需要採取動作" }, + { + "id": "native.apple.7205f0822706cb55", + "source": "Listening", + "translated": "正在聆聽" + }, + { + "id": "native.apple.8b10cb1dc21e42af", + "source": "Listening (PTT)", + "translated": "正在聆聽(PTT)" + }, + { + "id": "native.apple.939c7ec55636c67f", + "source": "Listening (Realtime)", + "translated": "正在聆聽(即時)" + }, + { + "id": "native.apple.86f203ebb2453d15", + "source": "Speaking…", + "translated": "說話中…" + }, + { + "id": "native.apple.1e3ff6b56ae3828d", + "source": "Speaking (System)…", + "translated": "正在朗讀(系統)…" + }, + { + "id": "native.apple.37a5fedaefb8ebf3", + "source": "Generating voice…", + "translated": "正在產生語音…" + }, + { + "id": "native.apple.47003f094346c660", + "source": "Ready", + "translated": "就緒" + }, { "id": "native.apple.cf0450b542893fb5", "source": "Off", @@ -15348,26 +15303,6 @@ "source": "unknown relay error", "translated": "unknown relay error" }, - { - "id": "native.apple.f2ea327bfea8b8e3", - "source": "Talk", - "translated": "交談" - }, - { - "id": "native.apple.bda6cfa988e49a75", - "source": "Control", - "translated": "控制" - }, - { - "id": "native.apple.3c80f5351055a5f3", - "source": "Agent", - "translated": "代理" - }, - { - "id": "native.apple.22d107db5b905ef4", - "source": "Settings", - "translated": "設定" - }, { "id": "native.apple.c23d35de8d70863f", "source": "OpenClaw", @@ -15458,6 +15393,26 @@ "source": "Close canvas", "translated": "關閉畫布" }, + { + "id": "native.apple.a1db6ce0aed1d18e", + "source": "Chat, voice active", + "translated": "聊天,語音已開啟" + }, + { + "id": "native.apple.bda6cfa988e49a75", + "source": "Control", + "translated": "控制" + }, + { + "id": "native.apple.3c80f5351055a5f3", + "source": "Agent", + "translated": "代理" + }, + { + "id": "native.apple.22d107db5b905ef4", + "source": "Settings", + "translated": "設定" + }, { "id": "native.apple.8a4751c86aa6fe77", "source": "Gateway needs attention", @@ -15488,11 +15443,6 @@ "source": "Chat", "translated": "聊天" }, - { - "id": "native.apple.febca7528af94342", - "source": "Talk", - "translated": "語音對話" - }, { "id": "native.apple.e26bbe303d8a9544", "source": "Overview", @@ -15863,6 +15813,11 @@ "source": "Requesting permissions…", "translated": "正在要求權限…" }, + { + "id": "native.apple.198f59c97d7afe11", + "source": "Start failed: %@", + "translated": "啟動失敗:%@" + }, { "id": "native.apple.a1910b5c2e008b6c", "source": "Microphone permission denied", @@ -15873,11 +15828,6 @@ "source": "Speech recognition", "translated": "語音辨識" }, - { - "id": "native.apple.198f59c97d7afe11", - "source": "Start failed: %@", - "translated": "啟動失敗:%@" - }, { "id": "native.apple.c09f4e3bbef8177a", "source": "Offline", @@ -16088,66 +16038,6 @@ "source": "Listening (PTT)", "translated": "正在聆聽(PTT)" }, - { - "id": "native.apple.0189269b6a863dc2", - "source": "Request ID", - "translated": "要求 ID" - }, - { - "id": "native.apple.f9d69f27f9d7f4de", - "source": "Sending...", - "translated": "正在傳送..." - }, - { - "id": "native.apple.0c5336129f70faed", - "source": "Retry", - "translated": "重試" - }, - { - "id": "native.apple.7b23f0a89d3ad411", - "source": "Sending approval request", - "translated": "正在傳送核准要求" - }, - { - "id": "native.apple.55db7317a2542e51", - "source": "Approval sent", - "translated": "已傳送核准" - }, - { - "id": "native.apple.06330f62ab254a0e", - "source": "Could not request approval", - "translated": "無法要求核准" - }, - { - "id": "native.apple.6580d3d1cdda682d", - "source": "Enable Talk", - "translated": "啟用 Talk" - }, - { - "id": "native.apple.abbfc1ba85a230b4", - "source": "Sending a new pairing request to your gateway...", - "translated": "正在將新的配對要求傳送至您的 gateway..." - }, - { - "id": "native.apple.2880deb53ea2fd86", - "source": "Approve this request on your gateway. Talk will start automatically when approval lands.", - "translated": "請在您的 gateway 上核准此要求。核准送達後,Talk 將自動開始。" - }, - { - "id": "native.apple.80faa829f543c03c", - "source": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider.", - "translated": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider." - }, - { - "id": "native.apple.88b0a2e2986fcebf", - "source": "Request Again", - "translated": "再次要求" - }, - { - "id": "native.apple.5a3faae3472b4b86", - "source": "Send Approval Request", - "translated": "傳送核准要求" - }, { "id": "native.apple.1065cfe92d7c1eb6", "source": "Asking OpenClaw", @@ -22908,11 +22798,6 @@ "source": "Attachments", "translated": "附件" }, - { - "id": "native.apple.f8dda832ed76441d", - "source": "Voice note", - "translated": "語音留言" - }, { "id": "native.apple.ee2c0d22f5861159", "source": "Talk", @@ -23898,6 +23783,11 @@ "source": "Dismiss", "translated": "關閉" }, + { + "id": "native.apple.3892b7f55d9d1f0a", + "source": "Remove attachments or wait for delivery to resolve before starting a new chat.", + "translated": "請先移除附件或等待傳送完成,再開始新的聊天。" + }, { "id": "native.apple.92fe9093903eda3b", "source": "Could not attach voice note: %@", @@ -23958,11 +23848,6 @@ "source": "Gateway changed before the session operation started.", "translated": "Gateway 在工作階段操作開始前已變更。" }, - { - "id": "native.apple.d93e6fe16ec1b0e5", - "source": "Remove attachments or wait for delivery to resolve before starting a new chat.", - "translated": "請先移除附件或等待傳送完成,再開始新的聊天。" - }, { "id": "native.apple.710c7bd117a7cc12", "source": "Remove attachments or wait for delivery to resolve before switching chats.", @@ -24188,6 +24073,61 @@ "source": "Compact Session", "translated": "精簡工作階段" }, + { + "id": "native.apple.fa10740f9e272e83", + "source": "Voice note", + "translated": "語音留言" + }, + { + "id": "native.apple.e4de5afc37981d6c", + "source": "Photo Library", + "translated": "照片圖庫" + }, + { + "id": "native.apple.37c19a66626b21fa", + "source": "Camera", + "translated": "相機" + }, + { + "id": "native.apple.333682684163ba6b", + "source": "Choose Image File", + "translated": "選擇圖片檔案" + }, + { + "id": "native.apple.534246792690968d", + "source": "Add attachment", + "translated": "加入附件" + }, + { + "id": "native.apple.e33210feaa3da016", + "source": "Record Voice Note", + "translated": "錄製語音留言" + }, + { + "id": "native.apple.00f0e15858e38d19", + "source": "Dictate message", + "translated": "語音輸入訊息" + }, + { + "id": "native.apple.96f20175f61a2b29", + "source": "Listening", + "translated": "正在聆聽" + }, + { + "id": "native.apple.eaa9047db6cfb67e", + "source": "Not listening", + "translated": "未在聆聽" + }, + { + "id": "native.apple.4185df7a940f09ef", + "source": "Finish dictation", + "translated": "結束語音輸入" + }, + { + "id": "native.apple.5a0793d2827b9399", + "source": "Transcribe speech into the message", + "translated": "將語音轉成訊息文字。" + }, { "id": "native.apple.f74d150ce05205dd", "source": "z", diff --git a/apps/android/app/src/main/res/values-ar/strings.xml b/apps/android/app/src/main/res/values-ar/strings.xml index b3c7f9659870..e5df535b5676 100644 --- a/apps/android/app/src/main/res/values-ar/strings.xml +++ b/apps/android/app/src/main/res/values-ar/strings.xml @@ -105,7 +105,7 @@ "لا توجد جلسات حديثة" "حالة Gateway، وجاهزية عقدة الهاتف، وتدفق السجلات الأخير." "فتح تفاصيل الأتمتة" - "وقت التشغيل" + "بيئة التشغيل" "الوكيل %1$s" "Get Goal" "OpenClaw %1$s (%2$s)" diff --git a/apps/android/app/src/main/res/values-it/strings.xml b/apps/android/app/src/main/res/values-it/strings.xml index 229ac71a5fc6..89729c045709 100644 --- a/apps/android/app/src/main/res/values-it/strings.xml +++ b/apps/android/app/src/main/res/values-it/strings.xml @@ -854,7 +854,7 @@ "Nessuna attività pianificata" "Stato" "Nodo OpenClaw · Connesso" - "Attivo" + "Attive" "Mostra lo stato di debug della condivisione dello schermo." "Nessun limite segnalato" "Chiudi scanner" diff --git a/apps/android/app/src/main/res/values-ja/strings.xml b/apps/android/app/src/main/res/values-ja/strings.xml index 3d1f31ff542e..288f803ad116 100644 --- a/apps/android/app/src/main/res/values-ja/strings.xml +++ b/apps/android/app/src/main/res/values-ja/strings.xml @@ -854,7 +854,7 @@ "スケジュール済みのジョブはありません" "ステータス" "OpenClaw Node · 接続済み" - "アクティブ" + "有効" "画面共有のデバッグ状態を表示します。" "制限は報告されていません" "スキャナーを閉じる" diff --git a/apps/android/app/src/main/res/values-pl/strings.xml b/apps/android/app/src/main/res/values-pl/strings.xml index 59e015988790..f11db2c14534 100644 --- a/apps/android/app/src/main/res/values-pl/strings.xml +++ b/apps/android/app/src/main/res/values-pl/strings.xml @@ -398,7 +398,7 @@ "screenshot" "Wyrażenie cron, np. 0 9 * * *" "Powrót do głosu" - "Rozmowa" + "Rozmawiaj" "%1$s/%2$s online" "Zezwolono %1$s aplikacjom na przekazywanie." "Czat" diff --git a/apps/android/app/src/main/res/values-tr/strings.xml b/apps/android/app/src/main/res/values-tr/strings.xml index 23b1f9467a4c..0fcffd616a27 100644 --- a/apps/android/app/src/main/res/values-tr/strings.xml +++ b/apps/android/app/src/main/res/values-tr/strings.xml @@ -866,7 +866,7 @@ "%1$s / %2$s" "Çalışmayı planlayın" "console" - "Yeniden dene" + "Yeniden Dene" "Bir sohbet başlatın; etkin OpenClaw konuşmalarınız burada görünecek." "Otomasyon yüklenemedi." "Aracı çalışma alanındaki kabuk" diff --git a/apps/android/app/src/main/res/values-zh-rTW/strings.xml b/apps/android/app/src/main/res/values-zh-rTW/strings.xml index a57c10fab84b..bd620ca1a1bd 100644 --- a/apps/android/app/src/main/res/values-zh-rTW/strings.xml +++ b/apps/android/app/src/main/res/values-zh-rTW/strings.xml @@ -854,7 +854,7 @@ "沒有排程工作" "狀態" "OpenClaw 節點 · 已連線" - "使用中" + "啟用中" "顯示螢幕分享偵錯狀態。" "未回報限制" "關閉掃描器" diff --git a/apps/ios/ActivityWidget/OpenClawActivityTypography.swift b/apps/ios/ActivityWidget/OpenClawActivityTypography.swift index 6c12f5f2b081..ea0f4b7dbefd 100644 --- a/apps/ios/ActivityWidget/OpenClawActivityTypography.swift +++ b/apps/ios/ActivityWidget/OpenClawActivityTypography.swift @@ -1,6 +1,10 @@ import SwiftUI enum OpenClawActivityType { + static var eyebrow: Font { + body(size: 8).weight(.bold) + } + static var subheadSemiBold: Font { display(size: 15, weight: .semibold) } diff --git a/apps/ios/ActivityWidget/OpenClawLiveActivity.swift b/apps/ios/ActivityWidget/OpenClawLiveActivity.swift index 377923fb4515..d2319fb5a906 100644 --- a/apps/ios/ActivityWidget/OpenClawLiveActivity.swift +++ b/apps/ios/ActivityWidget/OpenClawLiveActivity.swift @@ -1,73 +1,172 @@ import ActivityKit +import Foundation import SwiftUI import WidgetKit struct OpenClawLiveActivity: Widget { var body: some WidgetConfiguration { ActivityConfiguration(for: OpenClawActivityAttributes.self) { context in - self.lockScreenView(context: context) + self.lockScreenView(context: context, state: self.displayState(context: context)) } dynamicIsland: { context in - DynamicIsland { + let state = self.displayState(context: context) + return DynamicIsland { DynamicIslandExpandedRegion(.leading) { - self.statusDot(state: context.state) + self.agentAvatar(context: context, state: state) } DynamicIslandExpandedRegion(.center) { - self.statusText(state: context.state) - .font(OpenClawActivityType.subheadSemiBold) - .lineLimit(1) - .minimumScaleFactor(0.8) + VStack(alignment: .leading, spacing: 1) { + Text("OPENCLAW") + .font(OpenClawActivityType.eyebrow) + .foregroundStyle(OpenClawActivityStyle.coral) + self.statusText(state: state) + .font(OpenClawActivityType.subheadSemiBold) + .lineLimit(1) + .minimumScaleFactor(0.8) + } } DynamicIslandExpandedRegion(.trailing) { - self.trailingView(state: context.state) + if self.isVoiceSpeaking(state: state) { + Text("LIVE") + .font(OpenClawActivityType.eyebrow) + .foregroundStyle(OpenClawActivityStyle.sea) + } else { + self.trailingView(state: state) + } } + .contentMargins(.horizontal, 8) } compactLeading: { - self.statusDot(state: context.state) + self.agentAvatar(context: context, state: state, compact: true) } compactTrailing: { - self.compactStatusIcon(state: context.state) + self.compactTrailingView(state: state) } minimal: { - self.statusDot(state: context.state) + self.agentAvatar(context: context, state: state, compact: true) } + .keylineTint(self.islandKeylineTint(state: state)) } } - private func lockScreenView(context: ActivityViewContext) -> some View { + private func lockScreenView( + context: ActivityViewContext, + state: OpenClawActivityAttributes.ContentState) -> some View + { HStack(spacing: 10) { - self.statusIcon(state: context.state) - .frame(width: 30, height: 30) - .background(.thinMaterial, in: Circle()) + self.agentAvatar(context: context, state: state) VStack(alignment: .leading, spacing: 2) { Text("OpenClaw") .font(OpenClawActivityType.subheadBold) .lineLimit(1) - self.statusText(state: context.state) + self.statusText(state: state) .font(OpenClawActivityType.caption) .foregroundStyle(.secondary) .lineLimit(1) .minimumScaleFactor(0.8) } Spacer() - self.trailingView(state: context.state) + self.trailingView(state: state) } .padding(.horizontal, 12) .padding(.vertical, 8) } + @ViewBuilder private func trailingView(state: OpenClawActivityAttributes.ContentState) -> some View { - self.statusIcon(state: state) - .font(OpenClawActivityType.symbol(size: 16, weight: .semibold)) - .frame(width: 28, height: 28) + switch state.status { + case .voiceSpeaking: + self.voiceTrace(state: state) + .frame(width: 64, height: 20) + default: + self.statusIcon(state: state) + .font(OpenClawActivityType.symbol(size: 16, weight: .semibold)) + .frame(width: 28, height: 28) + } } - private func statusDot(state: OpenClawActivityAttributes.ContentState) -> some View { - Circle() - .fill(self.dotColor(state: state)) - .frame(width: 6, height: 6) + private func agentAvatar( + context: ActivityViewContext, + state: OpenClawActivityAttributes.ContentState, + compact: Bool = false) -> some View + { + TalkAvatarWaveformView( + phase: self.voicePhase(state: state), + palette: .liveActivity, + diameter: compact ? 30 : 42, + avatarDiameter: compact ? 16 : 28, + samples: self.voiceSamples(state: state)) + { + Text(verbatim: context.state.agentBadge ?? Self.initials(context.attributes.agentName)) + .font(OpenClawActivityType.symbol(size: compact ? 8 : 13, weight: .bold)) + .foregroundStyle(.white) + .minimumScaleFactor(0.6) + .lineLimit(1) + .frame(width: compact ? 16 : 28, height: compact ? 16 : 28) + .background(OpenClawActivityStyle.surfaceElevated, in: Circle()) + } + .accessibilityLabel(Text(verbatim: context.attributes.agentName)) } - private func compactStatusIcon(state: OpenClawActivityAttributes.ContentState) -> some View { - self.statusIcon(state: state) - .font(OpenClawActivityType.symbol(size: 12, weight: .semibold)) - .frame(width: 18, height: 18) + private func voicePhase(state: OpenClawActivityAttributes.ContentState) -> TalkWaveformPhase { + switch state.status { + case .voiceSpeaking: + .speaking(level: self.voiceSamples(state: state).last) + case .voiceListening: + .listening( + level: self.voiceSamples(state: state).last ?? 0.15, + speechActive: true) + case .voiceActive: + .idle + default: + .idle + } + } + + private static func initials(_ name: String) -> String { + let words = name.split(whereSeparator: \.isWhitespace) + let value = words.prefix(2).compactMap(\.first).map(String.init).joined() + return value.isEmpty ? "OC" : value.uppercased() + } + + @ViewBuilder + private func compactTrailingView(state: OpenClawActivityAttributes.ContentState) -> some View { + switch state.status { + case .voiceSpeaking: + Text("LIVE") + .font(OpenClawActivityType.eyebrow) + .foregroundStyle(OpenClawActivityStyle.sea) + default: + self.statusIcon(state: state) + .font(OpenClawActivityType.symbol(size: 12, weight: .semibold)) + .frame(width: 18, height: 18) + } + } + + private func voiceTrace( + state: OpenClawActivityAttributes.ContentState, + sampleRange: ClosedRange = 0...1) -> some View + { + TalkVoiceTraceView( + phase: self.voicePhase(state: state), + palette: .liveActivity, + samples: self.voiceSamples(state: state), + sampleRange: sampleRange) + } + + private func voiceSamples(state: OpenClawActivityAttributes.ContentState) -> [Double] { + (state.voiceSamples ?? []).map { Double($0) / 255 } + } + + private func isVoiceSpeaking(state: OpenClawActivityAttributes.ContentState) -> Bool { + state.status == .voiceSpeaking + } + + private func islandKeylineTint(state: OpenClawActivityAttributes.ContentState) -> Color? { + switch state.status { + case .voiceSpeaking: + OpenClawActivityStyle.coral + case .voiceListening, .voiceActive: + OpenClawActivityStyle.sea + default: + nil + } } @ViewBuilder @@ -79,12 +178,27 @@ struct OpenClawLiveActivity: Widget { case .disconnected: Image(systemName: "wifi.slash") .foregroundStyle(OpenClawActivityStyle.danger) + case .paused: + Image(systemName: "pause.fill") + .foregroundStyle(OpenClawActivityStyle.warn) case .idle: Image(systemName: "checkmark") .foregroundStyle(OpenClawActivityStyle.ok) case .approvalNeeded, .actionRequired, .attention: Image(systemName: "exclamationmark.triangle.fill") .foregroundStyle(OpenClawActivityStyle.warn) + case .toolRunning: + Image(systemName: "hammer.fill") + .foregroundStyle(OpenClawActivityStyle.info) + case .voiceListening: + Image(systemName: "mic.fill") + .foregroundStyle(OpenClawActivityStyle.sea) + case .voiceSpeaking: + Image(systemName: "waveform") + .foregroundStyle(OpenClawActivityStyle.coral) + case .voiceActive: + Image(systemName: "waveform") + .foregroundStyle(OpenClawActivityStyle.sea) } } @@ -97,28 +211,58 @@ struct OpenClawLiveActivity: Widget { case .reconnecting: Text("Reconnecting...") case .approvalNeeded: Text("Approval needed") case .actionRequired, .attention: Text("Action required") + case .toolRunning: + if let toolName = state.toolName { + Text(verbatim: String(format: String(localized: "Using %@"), toolName)) + } else { + Text("Using a tool") + } + case .voiceListening: Text("Listening") + case .voiceSpeaking: Text("Speaking") + case .voiceActive: Text("Voice active") + case .paused: Text("Paused") case .idle: Text("Connected") case .disconnected: Text("Disconnected") } } - private func dotColor(state: OpenClawActivityAttributes.ContentState) -> Color { - switch state.status { - case .connecting, .reconnecting: - OpenClawActivityStyle.info - case .disconnected: - OpenClawActivityStyle.danger - case .idle: - OpenClawActivityStyle.ok - case .approvalNeeded, .actionRequired, .attention: - OpenClawActivityStyle.warn + private func displayState( + context: ActivityViewContext) -> OpenClawActivityAttributes.ContentState + { + guard context.isStale else { return context.state } + switch context.state.status { + case .connecting, .reconnecting, .approvalNeeded, .actionRequired, .attention, + .toolRunning, .voiceListening, .voiceSpeaking, .voiceActive: + return OpenClawActivityAttributes.ContentState( + status: .paused, + verbatimDetail: nil, + startedAt: context.state.startedAt, + agentBadge: context.state.agentBadge) + case .paused, .idle, .disconnected: + return context.state } } } private enum OpenClawActivityStyle { - static let info = Color(red: 0, green: 122 / 255.0, blue: 1) + // Carapace dark-theme tokens, kept local because the widget is a separate target. + static let coral = Color(red: 245 / 255.0, green: 101 / 255.0, blue: 74 / 255.0) + static let sea = Color(red: 79 / 255.0, green: 200 / 255.0, blue: 174 / 255.0) + static let surfaceElevated = Color(red: 32 / 255.0, green: 32 / 255.0, blue: 36 / 255.0) + static let info = sea static let danger = Color(red: 185 / 255.0, green: 28 / 255.0, blue: 28 / 255.0) static let ok = Color(red: 34 / 255.0, green: 197 / 255.0, blue: 94 / 255.0) static let warn = Color(red: 245 / 255.0, green: 158 / 255.0, blue: 11 / 255.0) } + +extension TalkWaveformPalette { + fileprivate static let liveActivity = TalkWaveformPalette( + active: [ + OpenClawActivityStyle.coral, + OpenClawActivityStyle.sea, + ], + inactive: [ + OpenClawActivityStyle.surfaceElevated, + OpenClawActivityStyle.surfaceElevated, + ]) +} diff --git a/apps/ios/Resources/Localizable.xcstrings b/apps/ios/Resources/Localizable.xcstrings index ce809900e3e7..8915a93f68c6 100644 --- a/apps/ios/Resources/Localizable.xcstrings +++ b/apps/ios/Resources/Localizable.xcstrings @@ -9793,6 +9793,142 @@ } } }, + "Add attachment": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Add attachment" + } + }, + "zh-CN": { + "stringUnit": { + "state": "translated", + "value": "添加附件" + } + }, + "zh-TW": { + "stringUnit": { + "state": "translated", + "value": "加入附件" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Adicionar anexo" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Anhang hinzufügen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Añadir archivo adjunto" + } + }, + "ja-JP": { + "stringUnit": { + "state": "translated", + "value": "添付を追加" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "첨부 추가" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajouter une pièce jointe" + } + }, + "hi": { + "stringUnit": { + "state": "translated", + "value": "अटैचमेंट जोड़ें" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "إضافة مرفق" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Aggiungi allegato" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Ek ekle" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Додати вкладення" + } + }, + "id": { + "stringUnit": { + "state": "translated", + "value": "Tambahkan lampiran" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Dodaj załącznik" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "เพิ่มไฟล์แนบ" + } + }, + "vi": { + "stringUnit": { + "state": "translated", + "value": "Thêm tệp đính kèm" + } + }, + "nl": { + "stringUnit": { + "state": "translated", + "value": "Bijlage toevoegen" + } + }, + "fa": { + "stringUnit": { + "state": "translated", + "value": "افزودن پیوست" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Добавить вложение" + } + }, + "sv": { + "stringUnit": { + "state": "translated", + "value": "Lägg till bilaga" + } + } + } + }, "Add events with least privilege.": { "localizations": { "en": { @@ -17681,142 +17817,6 @@ } } }, - "Approval sent": { - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Approval sent" - } - }, - "zh-CN": { - "stringUnit": { - "state": "translated", - "value": "审批已发送" - } - }, - "zh-TW": { - "stringUnit": { - "state": "translated", - "value": "已傳送核准" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Aprovação enviada" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Genehmigung gesendet" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Aprobación enviada" - } - }, - "ja-JP": { - "stringUnit": { - "state": "translated", - "value": "承認を送信しました" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "승인이 전송됨" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Approbation envoyée" - } - }, - "hi": { - "stringUnit": { - "state": "translated", - "value": "स्वीकृति भेजी गई" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "تم إرسال الموافقة" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Approvazione inviata" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Onay gönderildi" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Схвалення надіслано" - } - }, - "id": { - "stringUnit": { - "state": "translated", - "value": "Persetujuan terkirim" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Zatwierdzenie wysłane" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "ส่งการอนุมัติแล้ว" - } - }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đã gửi phê duyệt" - } - }, - "nl": { - "stringUnit": { - "state": "translated", - "value": "Goedkeuring verzonden" - } - }, - "fa": { - "stringUnit": { - "state": "translated", - "value": "تأیید ارسال شد" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Подтверждение отправлено" - } - }, - "sv": { - "stringUnit": { - "state": "translated", - "value": "Godkännande skickat" - } - } - } - }, "Approval set to Always Allow.": { "localizations": { "en": { @@ -18633,142 +18633,6 @@ } } }, - "Approve this request on your gateway. Talk will start automatically when approval lands.": { - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Approve this request on your gateway. Talk will start automatically when approval lands." - } - }, - "zh-CN": { - "stringUnit": { - "state": "translated", - "value": "请在您的 gateway 上批准此请求。审批完成后,Talk 将自动启动。" - } - }, - "zh-TW": { - "stringUnit": { - "state": "translated", - "value": "請在您的 gateway 上核准此要求。核准送達後,Talk 將自動開始。" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Aprove esta solicitação no seu gateway. O Talk será iniciado automaticamente quando a aprovação chegar." - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Genehmige diese Anfrage auf deiner Gateway. Talk startet automatisch, sobald die Genehmigung eintrifft." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Aprueba esta solicitud en tu gateway. Talk se iniciará automáticamente cuando llegue la aprobación." - } - }, - "ja-JP": { - "stringUnit": { - "state": "translated", - "value": "Gateway でこのリクエストを承認してください。承認されると Talk が自動的に開始します。" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "Gateway에서 이 요청을 승인하세요. 승인이 완료되면 Talk가 자동으로 시작됩니다." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Approuvez cette demande sur votre gateway. Talk démarrera automatiquement une fois l’approbation reçue." - } - }, - "hi": { - "stringUnit": { - "state": "translated", - "value": "अपने gateway पर इस अनुरोध को स्वीकृत करें। स्वीकृति मिलते ही Talk अपने-आप शुरू हो जाएगा।" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "وافق على هذا الطلب على gateway الخاص بك. سيبدأ Talk تلقائيًا عند وصول الموافقة." - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Approva questa richiesta sul tuo gateway. Talk si avvierà automaticamente quando l'approvazione sarà ricevuta." - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Bu isteği gateway’inizde onaylayın. Onay geldiğinde Talk otomatik olarak başlayacak." - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Схваліть цей запит на вашому Gateway. Talk запуститься автоматично після отримання схвалення." - } - }, - "id": { - "stringUnit": { - "state": "translated", - "value": "Setujui permintaan ini di gateway Anda. Talk akan dimulai otomatis saat persetujuan diterima." - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Zatwierdź tę prośbę na swoim gateway. Talk uruchomi się automatycznie po otrzymaniu zatwierdzenia." - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "อนุมัติคำขอนี้บน gateway ของคุณ Talk จะเริ่มโดยอัตโนมัติเมื่อได้รับการอนุมัติ" - } - }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Phê duyệt yêu cầu này trên gateway của bạn. Talk sẽ tự động bắt đầu khi phê duyệt đến." - } - }, - "nl": { - "stringUnit": { - "state": "translated", - "value": "Keur dit verzoek goed op je gateway. Talk start automatisch zodra de goedkeuring binnenkomt." - } - }, - "fa": { - "stringUnit": { - "state": "translated", - "value": "این درخواست را در gateway خود تأیید کنید. پس از رسیدن تأیید، Talk به‌طور خودکار شروع می‌شود." - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Подтвердите этот запрос на вашем gateway. Talk запустится автоматически, когда будет получено подтверждение." - } - }, - "sv": { - "stringUnit": { - "state": "translated", - "value": "Godkänn den här förfrågan på din gateway. Talk startar automatiskt när godkännandet tas emot." - } - } - } - }, "Archive": { "localizations": { "en": { @@ -22713,142 +22577,6 @@ } } }, - "Background listening": { - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Background listening" - } - }, - "zh-CN": { - "stringUnit": { - "state": "translated", - "value": "后台监听" - } - }, - "zh-TW": { - "stringUnit": { - "state": "translated", - "value": "背景聆聽" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Escuta em segundo plano" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Mithören im Hintergrund" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Escucha en segundo plano" - } - }, - "ja-JP": { - "stringUnit": { - "state": "translated", - "value": "バックグラウンドでのリスニング" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "백그라운드 듣기" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Écoute en arrière-plan" - } - }, - "hi": { - "stringUnit": { - "state": "translated", - "value": "बैकग्राउंड में सुनना" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "الاستماع في الخلفية" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Ascolto in background" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Arka planda dinleme" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Фонове прослуховування" - } - }, - "id": { - "stringUnit": { - "state": "translated", - "value": "Mendengarkan di latar belakang" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Nasłuchiwanie w tle" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "การฟังในพื้นหลัง" - } - }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Nghe trong nền" - } - }, - "nl": { - "stringUnit": { - "state": "translated", - "value": "Luisteren op de achtergrond" - } - }, - "fa": { - "stringUnit": { - "state": "translated", - "value": "گوش‌دادن در پس‌زمینه" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Фоновое прослушивание" - } - }, - "sv": { - "stringUnit": { - "state": "translated", - "value": "Lyssning i bakgrunden" - } - } - } - }, "Background location requests and significant-change updates are allowed.": { "localizations": { "en": { @@ -25267,8 +24995,8 @@ }, "vi": { "stringUnit": { - "state": "new", - "value": "Camera" + "state": "translated", + "value": "Máy ảnh" } }, "nl": { @@ -28697,6 +28425,142 @@ } } }, + "Chat, voice active": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Chat, voice active" + } + }, + "zh-CN": { + "stringUnit": { + "state": "translated", + "value": "聊天,语音已开启" + } + }, + "zh-TW": { + "stringUnit": { + "state": "translated", + "value": "聊天,語音已開啟" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Chat, voz ativa" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Chat, Sprache aktiv" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Chat, voz activada" + } + }, + "ja-JP": { + "stringUnit": { + "state": "translated", + "value": "チャット、音声オン" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "채팅, 음성 켜짐" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Chat, voix activée" + } + }, + "hi": { + "stringUnit": { + "state": "translated", + "value": "चैट, आवाज़ चालू" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "دردشة، الصوت مفعّل" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Chat, voce attiva" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Sohbet, ses açık" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Чат, голос увімкнено" + } + }, + "id": { + "stringUnit": { + "state": "translated", + "value": "Chat, suara aktif" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Czat, głos włączony" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "แชท เปิดเสียงอยู่" + } + }, + "vi": { + "stringUnit": { + "state": "translated", + "value": "Chat, giọng nói đang bật" + } + }, + "nl": { + "stringUnit": { + "state": "translated", + "value": "Chat, stem actief" + } + }, + "fa": { + "stringUnit": { + "state": "translated", + "value": "چت، صدا فعال است" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Чат, голос включён" + } + }, + "sv": { + "stringUnit": { + "state": "translated", + "value": "Chatt, röst aktiv" + } + } + } + }, "Check OpenClaw status": { "localizations": { "en": { @@ -30193,6 +30057,142 @@ } } }, + "Choose Image File": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Choose Image File" + } + }, + "zh-CN": { + "stringUnit": { + "state": "translated", + "value": "选择图片文件" + } + }, + "zh-TW": { + "stringUnit": { + "state": "translated", + "value": "選擇圖片檔案" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Escolher arquivo de imagem" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Bilddatei auswählen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Elegir archivo de imagen" + } + }, + "ja-JP": { + "stringUnit": { + "state": "translated", + "value": "画像ファイルを選択" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "이미지 파일 선택" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisir un fichier image" + } + }, + "hi": { + "stringUnit": { + "state": "translated", + "value": "इमेज फ़ाइल चुनें" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "اختر ملف صورة" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scegli file immagine" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Görüntü Dosyası Seç" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Вибрати файл зображення" + } + }, + "id": { + "stringUnit": { + "state": "translated", + "value": "Pilih File Gambar" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Wybierz plik obrazu" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "เลือกรูปภาพ" + } + }, + "vi": { + "stringUnit": { + "state": "translated", + "value": "Chọn tệp hình ảnh" + } + }, + "nl": { + "stringUnit": { + "state": "translated", + "value": "Afbeeldingsbestand kiezen" + } + }, + "fa": { + "stringUnit": { + "state": "translated", + "value": "انتخاب فایل تصویر" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Выбрать файл изображения" + } + }, + "sv": { + "stringUnit": { + "state": "translated", + "value": "Välj bildfil" + } + } + } + }, "Choose a location mode to request iOS permission.": { "localizations": { "en": { @@ -32913,6 +32913,142 @@ } } }, + "Collapsed": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Collapsed" + } + }, + "zh-CN": { + "stringUnit": { + "state": "translated", + "value": "已收起" + } + }, + "zh-TW": { + "stringUnit": { + "state": "translated", + "value": "已收合" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Recolhido" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Eingeklappt" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Contraído" + } + }, + "ja-JP": { + "stringUnit": { + "state": "translated", + "value": "折りたたみ" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "접힘" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réduit" + } + }, + "hi": { + "stringUnit": { + "state": "translated", + "value": "संक्षिप्त" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "مطوي" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Compresso" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Daraltılmış" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Згорнуто" + } + }, + "id": { + "stringUnit": { + "state": "translated", + "value": "Ciut" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Zwinięte" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ยุบอยู่" + } + }, + "vi": { + "stringUnit": { + "state": "translated", + "value": "Thu gọn" + } + }, + "nl": { + "stringUnit": { + "state": "translated", + "value": "Ingeklapt" + } + }, + "fa": { + "stringUnit": { + "state": "translated", + "value": "جمع‌شده" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Свернуто" + } + }, + "sv": { + "stringUnit": { + "state": "translated", + "value": "Ihopfälld" + } + } + } + }, "Command": { "localizations": { "en": { @@ -38489,142 +38625,6 @@ } } }, - "Connect to your gateway to start a voice conversation.": { - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Connect to your gateway to start a voice conversation." - } - }, - "zh-CN": { - "stringUnit": { - "state": "translated", - "value": "连接到 Gateway 以开始语音对话。" - } - }, - "zh-TW": { - "stringUnit": { - "state": "translated", - "value": "連線至 Gateway 以開始語音對話。" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Conecte-se ao seu Gateway para iniciar uma conversa por voz." - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Verbinde dich mit deinem Gateway, um ein Sprachgespräch zu beginnen." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Conéctate a tu Gateway para iniciar una conversación de voz." - } - }, - "ja-JP": { - "stringUnit": { - "state": "translated", - "value": "音声会話を開始するには、Gatewayに接続してください。" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "음성 대화를 시작하려면 Gateway에 연결하세요." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Connectez-vous à votre Gateway pour démarrer une conversation vocale." - } - }, - "hi": { - "stringUnit": { - "state": "translated", - "value": "वॉइस वार्तालाप शुरू करने के लिए अपने Gateway से कनेक्ट करें।" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "اتصل بـ Gateway لبدء محادثة صوتية." - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Connettiti al Gateway per avviare una conversazione vocale." - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Sesli görüşme başlatmak için Gateway'inize bağlanın." - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Підключіться до свого шлюзу, щоб розпочати голосову розмову." - } - }, - "id": { - "stringUnit": { - "state": "translated", - "value": "Hubungkan ke gateway Anda untuk memulai percakapan suara." - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Połącz się z Gateway, aby rozpocząć rozmowę głosową." - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "เชื่อมต่อกับ Gateway ของคุณเพื่อเริ่มการสนทนาด้วยเสียง" - } - }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Kết nối với Gateway của bạn để bắt đầu cuộc trò chuyện bằng giọng nói." - } - }, - "nl": { - "stringUnit": { - "state": "translated", - "value": "Maak verbinding met je gateway om een spraakgesprek te starten." - } - }, - "fa": { - "stringUnit": { - "state": "translated", - "value": "برای شروع مکالمه صوتی به Gateway خود متصل شوید." - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Подключитесь к Gateway, чтобы начать голосовой разговор." - } - }, - "sv": { - "stringUnit": { - "state": "translated", - "value": "Anslut till din Gateway för att starta ett röstsamtal." - } - } - } - }, "Connected": { "localizations": { "en": { @@ -41753,278 +41753,6 @@ } } }, - "Controls": { - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Controls" - } - }, - "zh-CN": { - "stringUnit": { - "state": "translated", - "value": "控制" - } - }, - "zh-TW": { - "stringUnit": { - "state": "translated", - "value": "控制項" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Controles" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Steuerelemente" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Controles" - } - }, - "ja-JP": { - "stringUnit": { - "state": "translated", - "value": "コントロール" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "컨트롤" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Contrôles" - } - }, - "hi": { - "stringUnit": { - "state": "translated", - "value": "नियंत्रण" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "عناصر التحكم" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Controlli" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Denetimler" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Елементи керування" - } - }, - "id": { - "stringUnit": { - "state": "translated", - "value": "Kontrol" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Elementy sterujące" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "การควบคุม" - } - }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Điều khiển" - } - }, - "nl": { - "stringUnit": { - "state": "translated", - "value": "Bediening" - } - }, - "fa": { - "stringUnit": { - "state": "translated", - "value": "کنترل‌ها" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Элементы управления" - } - }, - "sv": { - "stringUnit": { - "state": "translated", - "value": "Kontroller" - } - } - } - }, - "Conversation": { - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Conversation" - } - }, - "zh-CN": { - "stringUnit": { - "state": "translated", - "value": "对话" - } - }, - "zh-TW": { - "stringUnit": { - "state": "translated", - "value": "對話" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Conversa" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Unterhaltung" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Conversación" - } - }, - "ja-JP": { - "stringUnit": { - "state": "translated", - "value": "会話" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "대화" - } - }, - "fr": { - "stringUnit": { - "state": "new", - "value": "Conversation" - } - }, - "hi": { - "stringUnit": { - "state": "translated", - "value": "बातचीत" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "المحادثة" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Conversazione" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Sohbet" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Розмова" - } - }, - "id": { - "stringUnit": { - "state": "translated", - "value": "Percakapan" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Konwersacja" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "การสนทนา" - } - }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cuộc trò chuyện" - } - }, - "nl": { - "stringUnit": { - "state": "translated", - "value": "Gesprek" - } - }, - "fa": { - "stringUnit": { - "state": "translated", - "value": "مکالمه" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Беседа" - } - }, - "sv": { - "stringUnit": { - "state": "translated", - "value": "Konversation" - } - } - } - }, "Copied": { "localizations": { "en": { @@ -46921,142 +46649,6 @@ } } }, - "Could not request approval": { - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Could not request approval" - } - }, - "zh-CN": { - "stringUnit": { - "state": "translated", - "value": "无法请求审批" - } - }, - "zh-TW": { - "stringUnit": { - "state": "translated", - "value": "無法要求核准" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Não foi possível solicitar aprovação" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Genehmigung konnte nicht angefordert werden" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "No se pudo solicitar la aprobación" - } - }, - "ja-JP": { - "stringUnit": { - "state": "translated", - "value": "承認をリクエストできませんでした" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "승인을 요청할 수 없음" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Impossible de demander l’approbation" - } - }, - "hi": { - "stringUnit": { - "state": "translated", - "value": "स्वीकृति का अनुरोध नहीं किया जा सका" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "تعذّر طلب الموافقة" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Impossibile richiedere l'approvazione" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Onay istenemedi" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Не вдалося надіслати запит на схвалення" - } - }, - "id": { - "stringUnit": { - "state": "translated", - "value": "Tidak dapat meminta persetujuan" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Nie udało się poprosić o zatwierdzenie" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "ไม่สามารถขออนุมัติได้" - } - }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không thể yêu cầu phê duyệt" - } - }, - "nl": { - "stringUnit": { - "state": "translated", - "value": "Kon geen goedkeuring aanvragen" - } - }, - "fa": { - "stringUnit": { - "state": "translated", - "value": "امکان درخواست تأیید وجود نداشت" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Не удалось запросить подтверждение" - } - }, - "sv": { - "stringUnit": { - "state": "translated", - "value": "Kunde inte begära godkännande" - } - } - } - }, "Could not review skill": { "localizations": { "en": { @@ -53857,142 +53449,6 @@ } } }, - "Demo Mode Only": { - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Demo Mode Only" - } - }, - "zh-CN": { - "stringUnit": { - "state": "translated", - "value": "仅演示模式" - } - }, - "zh-TW": { - "stringUnit": { - "state": "translated", - "value": "僅限示範模式" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Apenas modo de demonstração" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Nur Demomodus" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Solo modo demo" - } - }, - "ja-JP": { - "stringUnit": { - "state": "translated", - "value": "デモモードのみ" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "데모 모드 전용" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Mode démo uniquement" - } - }, - "hi": { - "stringUnit": { - "state": "translated", - "value": "केवल डेमो मोड" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "وضع العرض التوضيحي فقط" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Solo modalità demo" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Yalnızca Demo Modu" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Лише деморежим" - } - }, - "id": { - "stringUnit": { - "state": "translated", - "value": "Hanya Mode Demo" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Tylko tryb demonstracyjny" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "โหมดสาธิตเท่านั้น" - } - }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chỉ ở chế độ demo" - } - }, - "nl": { - "stringUnit": { - "state": "translated", - "value": "Alleen demomodus" - } - }, - "fa": { - "stringUnit": { - "state": "translated", - "value": "فقط حالت دمو" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Только деморежим" - } - }, - "sv": { - "stringUnit": { - "state": "translated", - "value": "Endast demoläge" - } - } - } - }, "Demo mode only": { "localizations": { "en": { @@ -55625,6 +55081,142 @@ } } }, + "Dictate message": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Dictate message" + } + }, + "zh-CN": { + "stringUnit": { + "state": "translated", + "value": "语音输入消息" + } + }, + "zh-TW": { + "stringUnit": { + "state": "translated", + "value": "語音輸入訊息" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ditar mensagem" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nachricht diktieren" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Dictar mensaje" + } + }, + "ja-JP": { + "stringUnit": { + "state": "translated", + "value": "メッセージを音声入力" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "메시지 받아쓰기" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Dicter le message" + } + }, + "hi": { + "stringUnit": { + "state": "translated", + "value": "संदेश बोलकर लिखें" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "إملاء الرسالة" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Detta messaggio" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Mesajı dikte et" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Продиктувати повідомлення" + } + }, + "id": { + "stringUnit": { + "state": "translated", + "value": "Dikte pesan" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Dyktuj wiadomość" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "พูดป้อนข้อความ" + } + }, + "vi": { + "stringUnit": { + "state": "translated", + "value": "Đọc để nhập tin nhắn" + } + }, + "nl": { + "stringUnit": { + "state": "translated", + "value": "Bericht dicteren" + } + }, + "fa": { + "stringUnit": { + "state": "translated", + "value": "دیکته کردن پیام" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Продиктовать сообщение" + } + }, + "sv": { + "stringUnit": { + "state": "translated", + "value": "Diktera meddelande" + } + } + } + }, "Direct": { "localizations": { "en": { @@ -62561,142 +62153,6 @@ } } }, - "Enable Talk": { - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Enable Talk" - } - }, - "zh-CN": { - "stringUnit": { - "state": "translated", - "value": "启用 Talk" - } - }, - "zh-TW": { - "stringUnit": { - "state": "translated", - "value": "啟用 Talk" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Ativar conversa" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Talk aktivieren" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Activar Talk" - } - }, - "ja-JP": { - "stringUnit": { - "state": "translated", - "value": "Talk を有効にする" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "Talk 활성화" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Activer Talk" - } - }, - "hi": { - "stringUnit": { - "state": "translated", - "value": "Talk सक्षम करें" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "تفعيل Talk" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Abilita Talk" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Konuşmayı Etkinleştir" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Увімкнути Talk" - } - }, - "id": { - "stringUnit": { - "state": "translated", - "value": "Aktifkan Talk" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Włącz Talk" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "เปิดใช้งาน Talk" - } - }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bật Talk" - } - }, - "nl": { - "stringUnit": { - "state": "translated", - "value": "Gesprek inschakelen" - } - }, - "fa": { - "stringUnit": { - "state": "translated", - "value": "فعال‌سازی Talk" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Включить Talk" - } - }, - "sv": { - "stringUnit": { - "state": "translated", - "value": "Aktivera Talk" - } - } - } - }, "Enable from iPhone": { "localizations": { "en": { @@ -65281,6 +64737,142 @@ } } }, + "Expanded": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Expanded" + } + }, + "zh-CN": { + "stringUnit": { + "state": "translated", + "value": "已展开" + } + }, + "zh-TW": { + "stringUnit": { + "state": "translated", + "value": "已展開" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Expandido" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ausgeklappt" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Expandido" + } + }, + "ja-JP": { + "stringUnit": { + "state": "translated", + "value": "展開" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "펼침" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Développé" + } + }, + "hi": { + "stringUnit": { + "state": "translated", + "value": "विस्तृत" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "موسّع" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Espanso" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Genişletilmiş" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Розгорнуто" + } + }, + "id": { + "stringUnit": { + "state": "translated", + "value": "Buka" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Rozwinięte" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ขยายอยู่" + } + }, + "vi": { + "stringUnit": { + "state": "translated", + "value": "Mở rộng" + } + }, + "nl": { + "stringUnit": { + "state": "translated", + "value": "Uitgeklapt" + } + }, + "fa": { + "stringUnit": { + "state": "translated", + "value": "بازشده" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Развернуто" + } + }, + "sv": { + "stringUnit": { + "state": "translated", + "value": "Utfälld" + } + } + } + }, "Expires": { "localizations": { "en": { @@ -67457,6 +67049,142 @@ } } }, + "Finish dictation": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Finish dictation" + } + }, + "zh-CN": { + "stringUnit": { + "state": "translated", + "value": "结束语音输入" + } + }, + "zh-TW": { + "stringUnit": { + "state": "translated", + "value": "結束語音輸入" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Encerrar ditado" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Diktat beenden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Finalizar dictado" + } + }, + "ja-JP": { + "stringUnit": { + "state": "translated", + "value": "音声入力を終了" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "받아쓰기 종료" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Terminer la dictée" + } + }, + "hi": { + "stringUnit": { + "state": "translated", + "value": "डिक्टेशन समाप्त करें" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "إنهاء الإملاء" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Termina dettatura" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Dikteyi bitir" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Завершити диктування" + } + }, + "id": { + "stringUnit": { + "state": "translated", + "value": "Selesai dikte" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Zakończ dyktowanie" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "เสร็จสิ้นการป้อนด้วยเสียง" + } + }, + "vi": { + "stringUnit": { + "state": "translated", + "value": "Kết thúc đọc" + } + }, + "nl": { + "stringUnit": { + "state": "translated", + "value": "Dicteren voltooien" + } + }, + "fa": { + "stringUnit": { + "state": "translated", + "value": "پایان دیکته" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Завершить диктовку" + } + }, + "sv": { + "stringUnit": { + "state": "translated", + "value": "Avsluta diktering" + } + } + } + }, "Finish notification setup to receive alerts when the app is not active.": { "localizations": { "en": { @@ -71809,138 +71537,138 @@ } } }, - "Gateway approval is required before this phone can capture voice.": { + "Gateway Settings": { "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Gateway approval is required before this phone can capture voice." + "value": "Gateway Settings" } }, "zh-CN": { "stringUnit": { "state": "translated", - "value": "此手机需要获得 Gateway 批准后才能采集语音。" + "value": "网关设置" } }, "zh-TW": { "stringUnit": { "state": "translated", - "value": "此手機必須先獲得 Gateway 核准,才能擷取語音。" + "value": "閘道設定" } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "A aprovação do Gateway é necessária para que este telefone possa capturar voz." + "value": "Ajustes do gateway" } }, "de": { "stringUnit": { "state": "translated", - "value": "Die Genehmigung durch das Gateway ist erforderlich, bevor dieses Telefon Sprache erfassen kann." + "value": "Gateway-Einstellungen" } }, "es": { "stringUnit": { "state": "translated", - "value": "Se requiere la aprobación del Gateway para que este teléfono pueda capturar voz." + "value": "Ajustes de la pasarela" } }, "ja-JP": { "stringUnit": { "state": "translated", - "value": "この電話で音声をキャプチャするには、Gatewayの承認が必要です。" + "value": "ゲートウェイ設定" } }, "ko": { "stringUnit": { "state": "translated", - "value": "이 휴대폰에서 음성을 캡처하려면 Gateway 승인이 필요합니다." + "value": "게이트웨이 설정" } }, "fr": { "stringUnit": { "state": "translated", - "value": "L’approbation du Gateway est requise avant que ce téléphone puisse enregistrer la voix." + "value": "Réglages de la passerelle" } }, "hi": { "stringUnit": { "state": "translated", - "value": "यह फ़ोन वॉइस कैप्चर कर सके, इससे पहले Gateway की मंज़ूरी आवश्यक है।" + "value": "गेटवे सेटिंग्स" } }, "ar": { "stringUnit": { "state": "translated", - "value": "يلزم الحصول على موافقة Gateway قبل أن يتمكن هذا الهاتف من التقاط الصوت." + "value": "إعدادات البوابة" } }, "it": { "stringUnit": { "state": "translated", - "value": "È necessaria l'approvazione del Gateway prima che questo telefono possa acquisire la voce." + "value": "Impostazioni gateway" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Bu telefonun ses yakalayabilmesi için Gateway onayı gereklidir." + "value": "Ağ Geçidi Ayarları" } }, "uk": { "stringUnit": { "state": "translated", - "value": "Перш ніж цей телефон зможе записувати голос, потрібне схвалення Gateway." + "value": "Налаштування шлюзу" } }, "id": { "stringUnit": { "state": "translated", - "value": "Persetujuan Gateway diperlukan sebelum ponsel ini dapat merekam suara." + "value": "Pengaturan Gateway" } }, "pl": { "stringUnit": { "state": "translated", - "value": "Zanim ten telefon będzie mógł przechwytywać głos, wymagane jest zatwierdzenie przez Gateway." + "value": "Ustawienia bramy" } }, "th": { "stringUnit": { "state": "translated", - "value": "ต้องได้รับการอนุมัติจาก Gateway ก่อนที่โทรศัพท์เครื่องนี้จะสามารถบันทึกเสียงได้" + "value": "การตั้งค่าเกตเวย์" } }, "vi": { "stringUnit": { "state": "translated", - "value": "Cần có sự phê duyệt của Gateway trước khi điện thoại này có thể thu âm giọng nói." + "value": "Cài đặt cổng kết nối" } }, "nl": { "stringUnit": { "state": "translated", - "value": "Goedkeuring van de gateway is vereist voordat deze telefoon spraak kan opnemen." + "value": "Gateway-instellingen" } }, "fa": { "stringUnit": { "state": "translated", - "value": "پیش از اینکه این تلفن بتواند صدا را ضبط کند، تأیید Gateway لازم است." + "value": "تنظیمات درگاه" } }, "ru": { "stringUnit": { "state": "translated", - "value": "Прежде чем этот телефон сможет записывать голос, требуется одобрение Gateway." + "value": "Настройки шлюза" } }, "sv": { "stringUnit": { "state": "translated", - "value": "Godkännande från Gateway krävs innan den här telefonen kan spela in röst." + "value": "Gateway-inställningar" } } } @@ -78065,6 +77793,142 @@ } } }, + "Hides the gateway status label": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Hides the gateway status label" + } + }, + "zh-CN": { + "stringUnit": { + "state": "translated", + "value": "隐藏网关状态标签" + } + }, + "zh-TW": { + "stringUnit": { + "state": "translated", + "value": "隱藏閘道狀態標籤" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Oculta o rótulo de status do gateway" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Blendet die Gateway-Statusbezeichnung aus" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Oculta la etiqueta de estado de la pasarela" + } + }, + "ja-JP": { + "stringUnit": { + "state": "translated", + "value": "ゲートウェイのステータスラベルを非表示" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "게이트웨이 상태 레이블 숨기기" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Masque le libellé d’état de la passerelle" + } + }, + "hi": { + "stringUnit": { + "state": "translated", + "value": "गेटवे स्थिति लेबल छिपाता है" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "يخفي تسمية حالة البوابة" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nasconde l’etichetta di stato del gateway" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Ağ geçidi durum etiketini gizler" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Приховує мітку стану шлюзу" + } + }, + "id": { + "stringUnit": { + "state": "translated", + "value": "Sembunyikan label status gateway" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Ukrywa etykietę stanu bramy" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ซ่อนป้ายสถานะเกตเวย์" + } + }, + "vi": { + "stringUnit": { + "state": "translated", + "value": "Ẩn nhãn trạng thái cổng kết nối" + } + }, + "nl": { + "stringUnit": { + "state": "translated", + "value": "Verbergt het gateway-statuslabel" + } + }, + "fa": { + "stringUnit": { + "state": "translated", + "value": "برچسب وضعیت درگاه را پنهان می‌کند" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Скрывает метку состояния шлюза" + } + }, + "sv": { + "stringUnit": { + "state": "translated", + "value": "Döljer gateway-statusetiketten" + } + } + } + }, "High": { "localizations": { "en": { @@ -84593,6 +84457,142 @@ } } }, + "LIVE": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "LIVE" + } + }, + "zh-CN": { + "stringUnit": { + "state": "new", + "value": "LIVE" + } + }, + "zh-TW": { + "stringUnit": { + "state": "new", + "value": "LIVE" + } + }, + "pt-BR": { + "stringUnit": { + "state": "new", + "value": "LIVE" + } + }, + "de": { + "stringUnit": { + "state": "new", + "value": "LIVE" + } + }, + "es": { + "stringUnit": { + "state": "new", + "value": "LIVE" + } + }, + "ja-JP": { + "stringUnit": { + "state": "new", + "value": "LIVE" + } + }, + "ko": { + "stringUnit": { + "state": "new", + "value": "LIVE" + } + }, + "fr": { + "stringUnit": { + "state": "new", + "value": "LIVE" + } + }, + "hi": { + "stringUnit": { + "state": "new", + "value": "LIVE" + } + }, + "ar": { + "stringUnit": { + "state": "new", + "value": "LIVE" + } + }, + "it": { + "stringUnit": { + "state": "new", + "value": "LIVE" + } + }, + "tr": { + "stringUnit": { + "state": "new", + "value": "LIVE" + } + }, + "uk": { + "stringUnit": { + "state": "new", + "value": "LIVE" + } + }, + "id": { + "stringUnit": { + "state": "new", + "value": "LIVE" + } + }, + "pl": { + "stringUnit": { + "state": "new", + "value": "LIVE" + } + }, + "th": { + "stringUnit": { + "state": "new", + "value": "LIVE" + } + }, + "vi": { + "stringUnit": { + "state": "new", + "value": "LIVE" + } + }, + "nl": { + "stringUnit": { + "state": "new", + "value": "LIVE" + } + }, + "fa": { + "stringUnit": { + "state": "new", + "value": "LIVE" + } + }, + "ru": { + "stringUnit": { + "state": "new", + "value": "LIVE" + } + }, + "sv": { + "stringUnit": { + "state": "new", + "value": "LIVE" + } + } + } + }, "Last Run": { "localizations": { "en": { @@ -85137,142 +85137,6 @@ } } }, - "Last issue": { - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Last issue" - } - }, - "zh-CN": { - "stringUnit": { - "state": "translated", - "value": "上次问题" - } - }, - "zh-TW": { - "stringUnit": { - "state": "translated", - "value": "上次問題" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Último problema" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Letztes Problem" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Último problema" - } - }, - "ja-JP": { - "stringUnit": { - "state": "translated", - "value": "最後の問題" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "마지막 문제" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Dernier problème" - } - }, - "hi": { - "stringUnit": { - "state": "translated", - "value": "पिछली समस्या" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "آخر مشكلة" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Ultimo problema" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Son sorun" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Остання проблема" - } - }, - "id": { - "stringUnit": { - "state": "translated", - "value": "Masalah terakhir" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Ostatni problem" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "ปัญหาล่าสุด" - } - }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Sự cố gần nhất" - } - }, - "nl": { - "stringUnit": { - "state": "translated", - "value": "Laatste probleem" - } - }, - "fa": { - "stringUnit": { - "state": "translated", - "value": "آخرین مشکل" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Последняя проблема" - } - }, - "sv": { - "stringUnit": { - "state": "translated", - "value": "Senaste problem" - } - } - } - }, "Last run": { "localizations": { "en": { @@ -110025,6 +109889,142 @@ } } }, + "Not listening": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Not listening" + } + }, + "zh-CN": { + "stringUnit": { + "state": "translated", + "value": "未在收听" + } + }, + "zh-TW": { + "stringUnit": { + "state": "translated", + "value": "未在聆聽" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não está ouvindo" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Hört nicht zu" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No está escuchando" + } + }, + "ja-JP": { + "stringUnit": { + "state": "translated", + "value": "待機中" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "듣는 중 아님" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "N’écoute pas" + } + }, + "hi": { + "stringUnit": { + "state": "translated", + "value": "नहीं सुन रहा" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "لا يستمع" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Non in ascolto" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Dinlemiyor" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Не слухає" + } + }, + "id": { + "stringUnit": { + "state": "translated", + "value": "Tidak mendengarkan" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Nie nasłuchuje" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ไม่ได้ฟังอยู่" + } + }, + "vi": { + "stringUnit": { + "state": "translated", + "value": "Không nghe" + } + }, + "nl": { + "stringUnit": { + "state": "translated", + "value": "Luistert niet" + } + }, + "fa": { + "stringUnit": { + "state": "translated", + "value": "در حال گوش دادن نیست" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Не слушает" + } + }, + "sv": { + "stringUnit": { + "state": "translated", + "value": "Lyssnar inte" + } + } + } + }, "Not loaded": { "localizations": { "en": { @@ -111521,6 +111521,142 @@ } } }, + "OPENCLAW": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "OPENCLAW" + } + }, + "zh-CN": { + "stringUnit": { + "state": "new", + "value": "OPENCLAW" + } + }, + "zh-TW": { + "stringUnit": { + "state": "new", + "value": "OPENCLAW" + } + }, + "pt-BR": { + "stringUnit": { + "state": "new", + "value": "OPENCLAW" + } + }, + "de": { + "stringUnit": { + "state": "new", + "value": "OPENCLAW" + } + }, + "es": { + "stringUnit": { + "state": "new", + "value": "OPENCLAW" + } + }, + "ja-JP": { + "stringUnit": { + "state": "new", + "value": "OPENCLAW" + } + }, + "ko": { + "stringUnit": { + "state": "new", + "value": "OPENCLAW" + } + }, + "fr": { + "stringUnit": { + "state": "new", + "value": "OPENCLAW" + } + }, + "hi": { + "stringUnit": { + "state": "new", + "value": "OPENCLAW" + } + }, + "ar": { + "stringUnit": { + "state": "new", + "value": "OPENCLAW" + } + }, + "it": { + "stringUnit": { + "state": "new", + "value": "OPENCLAW" + } + }, + "tr": { + "stringUnit": { + "state": "new", + "value": "OPENCLAW" + } + }, + "uk": { + "stringUnit": { + "state": "new", + "value": "OPENCLAW" + } + }, + "id": { + "stringUnit": { + "state": "new", + "value": "OPENCLAW" + } + }, + "pl": { + "stringUnit": { + "state": "new", + "value": "OPENCLAW" + } + }, + "th": { + "stringUnit": { + "state": "new", + "value": "OPENCLAW" + } + }, + "vi": { + "stringUnit": { + "state": "new", + "value": "OPENCLAW" + } + }, + "nl": { + "stringUnit": { + "state": "new", + "value": "OPENCLAW" + } + }, + "fa": { + "stringUnit": { + "state": "new", + "value": "OPENCLAW" + } + }, + "ru": { + "stringUnit": { + "state": "new", + "value": "OPENCLAW" + } + }, + "sv": { + "stringUnit": { + "state": "new", + "value": "OPENCLAW" + } + } + } + }, "OS: %@": { "localizations": { "en": { @@ -114377,278 +114513,6 @@ } } }, - "Open Voice Settings": { - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Open Voice Settings" - } - }, - "zh-CN": { - "stringUnit": { - "state": "translated", - "value": "打开语音设置" - } - }, - "zh-TW": { - "stringUnit": { - "state": "translated", - "value": "開啟語音設定" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Abrir configurações de voz" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Spracheinstellungen öffnen" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Abrir configuración de voz" - } - }, - "ja-JP": { - "stringUnit": { - "state": "translated", - "value": "音声設定を開く" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "음성 설정 열기" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Ouvrir les paramètres vocaux" - } - }, - "hi": { - "stringUnit": { - "state": "translated", - "value": "Voice सेटिंग्स खोलें" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "فتح إعدادات الصوت" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Apri impostazioni voce" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Ses Ayarlarını Aç" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Відкрити налаштування голосу" - } - }, - "id": { - "stringUnit": { - "state": "translated", - "value": "Buka Pengaturan Suara" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Otwórz ustawienia głosu" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "เปิดการตั้งค่าเสียง" - } - }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Mở cài đặt giọng nói" - } - }, - "nl": { - "stringUnit": { - "state": "translated", - "value": "Spraakinstellingen openen" - } - }, - "fa": { - "stringUnit": { - "state": "translated", - "value": "باز کردن تنظیمات صدا" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Открыть настройки голоса" - } - }, - "sv": { - "stringUnit": { - "state": "translated", - "value": "Öppna röstinställningar" - } - } - } - }, - "Open Voice settings after the gateway loads Talk configuration.": { - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Open Voice settings after the gateway loads Talk configuration." - } - }, - "zh-CN": { - "stringUnit": { - "state": "translated", - "value": "Gateway 加载 Talk 配置后,打开语音设置。" - } - }, - "zh-TW": { - "stringUnit": { - "state": "translated", - "value": "Gateway 載入 Talk 設定後,開啟語音設定。" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Abra os ajustes de Voz depois que o Gateway carregar a configuração do Talk." - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Öffne die Spracheinstellungen, nachdem das Gateway die Talk-Konfiguration geladen hat." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Abre los ajustes de voz después de que el Gateway cargue la configuración de Talk." - } - }, - "ja-JP": { - "stringUnit": { - "state": "translated", - "value": "GatewayがTalk設定を読み込んだ後、音声設定を開いてください。" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "Gateway에서 Talk 구성을 불러온 후 음성 설정을 여세요." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Ouvrez les réglages de la voix une fois que le Gateway a chargé la configuration Talk." - } - }, - "hi": { - "stringUnit": { - "state": "translated", - "value": "Gateway द्वारा Talk कॉन्फ़िगरेशन लोड किए जाने के बाद वॉइस सेटिंग्स खोलें।" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "افتح إعدادات الصوت بعد أن يحمّل Gateway إعدادات Talk." - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Apri le impostazioni Voce dopo che il Gateway ha caricato la configurazione di Talk." - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Gateway, Talk yapılandırmasını yükledikten sonra Ses ayarlarını açın." - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Відкрийте налаштування голосу після того, як шлюз завантажить конфігурацію Talk." - } - }, - "id": { - "stringUnit": { - "state": "translated", - "value": "Buka pengaturan Suara setelah gateway memuat konfigurasi Talk." - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Otwórz ustawienia głosu po wczytaniu konfiguracji Talk przez Gateway." - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "เปิดการตั้งค่าเสียงหลังจาก Gateway โหลดการกำหนดค่า Talk แล้ว" - } - }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Mở phần cài đặt Giọng nói sau khi Gateway tải cấu hình Talk." - } - }, - "nl": { - "stringUnit": { - "state": "translated", - "value": "Open de spraakinstellingen nadat de gateway de Talk-configuratie heeft geladen." - } - }, - "fa": { - "stringUnit": { - "state": "translated", - "value": "پس از اینکه Gateway پیکربندی Talk را بارگیری کرد، تنظیمات صدا را باز کنید." - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Откройте настройки голоса после того, как Gateway загрузит конфигурацию Talk." - } - }, - "sv": { - "stringUnit": { - "state": "translated", - "value": "Öppna röstinställningarna efter att Gateway har läst in Talk-konfigurationen." - } - } - } - }, "Open all approvals": { "localizations": { "en": { @@ -121177,142 +121041,6 @@ } } }, - "Permission": { - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Permission" - } - }, - "zh-CN": { - "stringUnit": { - "state": "translated", - "value": "权限" - } - }, - "zh-TW": { - "stringUnit": { - "state": "translated", - "value": "權限" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Permissão" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Berechtigung" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Permiso" - } - }, - "ja-JP": { - "stringUnit": { - "state": "translated", - "value": "権限" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "권한" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Autorisation" - } - }, - "hi": { - "stringUnit": { - "state": "translated", - "value": "अनुमति" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "الإذن" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Autorizzazione" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "İzin" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Дозвіл" - } - }, - "id": { - "stringUnit": { - "state": "translated", - "value": "Izin" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Uprawnienie" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "สิทธิ์อนุญาต" - } - }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Quyền" - } - }, - "nl": { - "stringUnit": { - "state": "translated", - "value": "Toestemming" - } - }, - "fa": { - "stringUnit": { - "state": "translated", - "value": "مجوز" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Разрешение" - } - }, - "sv": { - "stringUnit": { - "state": "translated", - "value": "Behörighet" - } - } - } - }, "Permission can be saved": { "localizations": { "en": { @@ -121857,6 +121585,142 @@ } } }, + "Photo Library": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Photo Library" + } + }, + "zh-CN": { + "stringUnit": { + "state": "translated", + "value": "照片图库" + } + }, + "zh-TW": { + "stringUnit": { + "state": "translated", + "value": "照片圖庫" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Fototeca" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Fotomediathek" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Fototeca" + } + }, + "ja-JP": { + "stringUnit": { + "state": "translated", + "value": "写真ライブラリ" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "사진 보관함" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Photothèque" + } + }, + "hi": { + "stringUnit": { + "state": "translated", + "value": "फ़ोटो लाइब्रेरी" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "مكتبة الصور" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Libreria foto" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Fotoğraf Arşivi" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Фототека" + } + }, + "id": { + "stringUnit": { + "state": "translated", + "value": "Perpustakaan Foto" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Biblioteka zdjęć" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "คลังรูปภาพ" + } + }, + "vi": { + "stringUnit": { + "state": "translated", + "value": "Thư viện ảnh" + } + }, + "nl": { + "stringUnit": { + "state": "translated", + "value": "Fotobibliotheek" + } + }, + "fa": { + "stringUnit": { + "state": "translated", + "value": "کتابخانه عکس" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Медиатека" + } + }, + "sv": { + "stringUnit": { + "state": "translated", + "value": "Bildbibliotek" + } + } + } + }, "Photos": { "localizations": { "en": { @@ -128793,142 +128657,6 @@ } } }, - "Realtime voice and controls.": { - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Realtime voice and controls." - } - }, - "zh-CN": { - "stringUnit": { - "state": "translated", - "value": "实时语音和控制。" - } - }, - "zh-TW": { - "stringUnit": { - "state": "translated", - "value": "即時語音與控制。" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Voz e controles em tempo real." - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Echtzeit-Sprache und -Steuerung." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Voz y controles en tiempo real." - } - }, - "ja-JP": { - "stringUnit": { - "state": "translated", - "value": "リアルタイム音声とコントロール。" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "실시간 음성 및 제어." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Voix et commandes en temps réel." - } - }, - "hi": { - "stringUnit": { - "state": "translated", - "value": "रीयलटाइम आवाज़ और नियंत्रण।" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "الصوت والتحكم في الوقت الفعلي." - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Voce e controlli in tempo reale." - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Gerçek zamanlı ses ve kontroller." - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Голос і керування в реальному часі." - } - }, - "id": { - "stringUnit": { - "state": "translated", - "value": "Suara realtime dan kontrol." - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Głos i sterowanie w czasie rzeczywistym." - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "เสียงและการควบคุมแบบเรียลไทม์" - } - }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Giọng nói và điều khiển thời gian thực." - } - }, - "nl": { - "stringUnit": { - "state": "translated", - "value": "Realtime spraak en bediening." - } - }, - "fa": { - "stringUnit": { - "state": "translated", - "value": "صدای بلادرنگ و کنترل‌ها." - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Голос и управление в реальном времени." - } - }, - "sv": { - "stringUnit": { - "state": "translated", - "value": "Realtidsröst och kontroller." - } - } - } - }, "Realtime voice did not start.": { "localizations": { "en": { @@ -135729,278 +135457,6 @@ } } }, - "Request Again": { - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Request Again" - } - }, - "zh-CN": { - "stringUnit": { - "state": "translated", - "value": "再次请求" - } - }, - "zh-TW": { - "stringUnit": { - "state": "translated", - "value": "再次要求" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Solicitar novamente" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Erneut anfordern" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Solicitar de nuevo" - } - }, - "ja-JP": { - "stringUnit": { - "state": "translated", - "value": "再リクエスト" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "다시 요청" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Demander à nouveau" - } - }, - "hi": { - "stringUnit": { - "state": "translated", - "value": "फिर से अनुरोध करें" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "الطلب مرة أخرى" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Richiedi di nuovo" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Tekrar iste" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Запитати знову" - } - }, - "id": { - "stringUnit": { - "state": "translated", - "value": "Minta Lagi" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Poproś ponownie" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "ขออีกครั้ง" - } - }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Yêu cầu lại" - } - }, - "nl": { - "stringUnit": { - "state": "translated", - "value": "Opnieuw aanvragen" - } - }, - "fa": { - "stringUnit": { - "state": "translated", - "value": "درخواست دوباره" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Запросить снова" - } - }, - "sv": { - "stringUnit": { - "state": "translated", - "value": "Begär igen" - } - } - } - }, - "Request ID": { - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Request ID" - } - }, - "zh-CN": { - "stringUnit": { - "state": "translated", - "value": "请求 ID" - } - }, - "zh-TW": { - "stringUnit": { - "state": "translated", - "value": "要求 ID" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "ID da solicitação" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Anfrage-ID" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "ID de solicitud" - } - }, - "ja-JP": { - "stringUnit": { - "state": "translated", - "value": "リクエスト ID" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "요청 ID" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "ID de la demande" - } - }, - "hi": { - "stringUnit": { - "state": "translated", - "value": "अनुरोध ID" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "معرّف الطلب" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "ID richiesta" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "İstek kimliği" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "ID запиту" - } - }, - "id": { - "stringUnit": { - "state": "translated", - "value": "ID Permintaan" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "ID żądania" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "ID คำขอ" - } - }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "ID yêu cầu" - } - }, - "nl": { - "stringUnit": { - "state": "translated", - "value": "Aanvraag-ID" - } - }, - "fa": { - "stringUnit": { - "state": "translated", - "value": "شناسه درخواست" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "ID запроса" - } - }, - "sv": { - "stringUnit": { - "state": "translated", - "value": "Förfrågnings-ID" - } - } - } - }, "Request ID: %@": { "localizations": { "en": { @@ -141441,142 +140897,6 @@ } } }, - "Routes voice to %@.": { - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Routes voice to %@." - } - }, - "zh-CN": { - "stringUnit": { - "state": "translated", - "value": "将语音路由到 %@。" - } - }, - "zh-TW": { - "stringUnit": { - "state": "translated", - "value": "將語音導向 %@。" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Encaminha a voz para %@." - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Leitet Sprache an %@ weiter." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Dirige la voz a %@." - } - }, - "ja-JP": { - "stringUnit": { - "state": "translated", - "value": "音声を%@にルーティングします。" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "음성을 %@로 라우팅합니다." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Achemine la voix vers %@." - } - }, - "hi": { - "stringUnit": { - "state": "translated", - "value": "वॉइस को %@ पर रूट करता है।" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "يوجّه الصوت إلى %@." - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Instrada la voce verso %@." - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Sesi %@ hedefine yönlendirir." - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Спрямовує голос до %@." - } - }, - "id": { - "stringUnit": { - "state": "translated", - "value": "Merutekan suara ke %@." - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Kieruje głos do %@." - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "กำหนดเส้นทางเสียงไปยัง %@." - } - }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Định tuyến giọng nói đến %@." - } - }, - "nl": { - "stringUnit": { - "state": "translated", - "value": "Leidt spraak door naar %@." - } - }, - "fa": { - "stringUnit": { - "state": "translated", - "value": "صدا را به %@ هدایت می‌کند." - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Направляет голосовой ввод в %@." - } - }, - "sv": { - "stringUnit": { - "state": "translated", - "value": "Dirigerar röst till %@." - } - } - } - }, "Run": { "localizations": { "en": { @@ -150961,142 +150281,6 @@ } } }, - "Send Approval Request": { - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Send Approval Request" - } - }, - "zh-CN": { - "stringUnit": { - "state": "translated", - "value": "发送审批请求" - } - }, - "zh-TW": { - "stringUnit": { - "state": "translated", - "value": "傳送核准要求" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Enviar solicitação de aprovação" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Genehmigungsanfrage senden" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Enviar solicitud de aprobación" - } - }, - "ja-JP": { - "stringUnit": { - "state": "translated", - "value": "承認リクエストを送信" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "승인 요청 보내기" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Envoyer une demande d’approbation" - } - }, - "hi": { - "stringUnit": { - "state": "translated", - "value": "स्वीकृति अनुरोध भेजें" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "إرسال طلب الموافقة" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Invia richiesta di approvazione" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Onay isteği gönder" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Надіслати запит на схвалення" - } - }, - "id": { - "stringUnit": { - "state": "translated", - "value": "Kirim Permintaan Persetujuan" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Wyślij prośbę o zatwierdzenie" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "ส่งคำขออนุมัติ" - } - }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Gửi yêu cầu phê duyệt" - } - }, - "nl": { - "stringUnit": { - "state": "translated", - "value": "Goedkeuringsaanvraag verzenden" - } - }, - "fa": { - "stringUnit": { - "state": "translated", - "value": "ارسال درخواست تأیید" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Отправить запрос на подтверждение" - } - }, - "sv": { - "stringUnit": { - "state": "translated", - "value": "Skicka godkännandeförfrågan" - } - } - } - }, "Send from watch": { "localizations": { "en": { @@ -151641,278 +150825,6 @@ } } }, - "Sending a new pairing request to your gateway...": { - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Sending a new pairing request to your gateway..." - } - }, - "zh-CN": { - "stringUnit": { - "state": "translated", - "value": "正在向您的 gateway 发送新的配对请求..." - } - }, - "zh-TW": { - "stringUnit": { - "state": "translated", - "value": "正在將新的配對要求傳送至您的 gateway..." - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Enviando uma nova solicitação de pareamento para seu gateway..." - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Eine neue Kopplungsanfrage wird an deine Gateway gesendet..." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Enviando una nueva solicitud de emparejamiento a tu gateway..." - } - }, - "ja-JP": { - "stringUnit": { - "state": "translated", - "value": "Gateway に新しいペアリングリクエストを送信しています..." - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "Gateway에 새 페어링 요청을 전송하는 중..." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Envoi d’une nouvelle demande d’association à votre gateway..." - } - }, - "hi": { - "stringUnit": { - "state": "translated", - "value": "आपके gateway को नया pairing अनुरोध भेजा जा रहा है..." - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "جارٍ إرسال طلب اقتران جديد إلى gateway الخاص بك..." - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Invio di una nuova richiesta di abbinamento al tuo gateway..." - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Gateway’inize yeni bir eşleştirme isteği gönderiliyor..." - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Надсилання нового запиту на сполучення до вашого Gateway..." - } - }, - "id": { - "stringUnit": { - "state": "translated", - "value": "Mengirim permintaan penyandingan baru ke gateway Anda..." - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Wysyłanie nowej prośby o parowanie do Twojego gateway..." - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "กำลังส่งคำขอจับคู่ใหม่ไปยัง gateway ของคุณ..." - } - }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đang gửi yêu cầu ghép đôi mới đến gateway của bạn..." - } - }, - "nl": { - "stringUnit": { - "state": "translated", - "value": "Een nieuw koppelingsverzoek naar je gateway verzenden..." - } - }, - "fa": { - "stringUnit": { - "state": "translated", - "value": "در حال ارسال درخواست جفت‌سازی جدید به gateway شما..." - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Отправка нового запроса на сопряжение вашему gateway..." - } - }, - "sv": { - "stringUnit": { - "state": "translated", - "value": "Skickar en ny parkopplingsförfrågan till din gateway..." - } - } - } - }, - "Sending approval request": { - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Sending approval request" - } - }, - "zh-CN": { - "stringUnit": { - "state": "translated", - "value": "正在发送审批请求" - } - }, - "zh-TW": { - "stringUnit": { - "state": "translated", - "value": "正在傳送核准要求" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Enviando solicitação de aprovação" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Genehmigungsanfrage wird gesendet" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Enviando solicitud de aprobación" - } - }, - "ja-JP": { - "stringUnit": { - "state": "translated", - "value": "承認リクエストを送信中" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "승인 요청 전송 중" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Envoi de la demande d’approbation" - } - }, - "hi": { - "stringUnit": { - "state": "translated", - "value": "स्वीकृति अनुरोध भेजा जा रहा है" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "جارٍ إرسال طلب الموافقة" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Invio della richiesta di approvazione" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Onay isteği gönderiliyor" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Надсилання запиту на схвалення" - } - }, - "id": { - "stringUnit": { - "state": "translated", - "value": "Mengirim permintaan persetujuan" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Wysyłanie prośby o zatwierdzenie" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "กำลังส่งคำขออนุมัติ" - } - }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đang gửi yêu cầu phê duyệt" - } - }, - "nl": { - "stringUnit": { - "state": "translated", - "value": "Goedkeuringsaanvraag verzenden" - } - }, - "fa": { - "stringUnit": { - "state": "translated", - "value": "در حال ارسال درخواست تأیید" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Отправка запроса на подтверждение" - } - }, - "sv": { - "stringUnit": { - "state": "translated", - "value": "Skickar godkännandeförfrågan" - } - } - } - }, "Sending decision...": { "localizations": { "en": { @@ -152049,142 +150961,6 @@ } } }, - "Sending...": { - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Sending..." - } - }, - "zh-CN": { - "stringUnit": { - "state": "translated", - "value": "正在发送..." - } - }, - "zh-TW": { - "stringUnit": { - "state": "translated", - "value": "正在傳送..." - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Enviando..." - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Wird gesendet..." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Enviando..." - } - }, - "ja-JP": { - "stringUnit": { - "state": "translated", - "value": "送信中..." - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "전송 중..." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Envoi..." - } - }, - "hi": { - "stringUnit": { - "state": "translated", - "value": "भेजा जा रहा है..." - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "جارٍ الإرسال..." - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Invio..." - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Gönderiliyor..." - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Надсилання..." - } - }, - "id": { - "stringUnit": { - "state": "translated", - "value": "Mengirim..." - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Wysyłanie..." - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "กำลังส่ง..." - } - }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đang gửi..." - } - }, - "nl": { - "stringUnit": { - "state": "translated", - "value": "Verzenden..." - } - }, - "fa": { - "stringUnit": { - "state": "translated", - "value": "در حال ارسال..." - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Отправка..." - } - }, - "sv": { - "stringUnit": { - "state": "translated", - "value": "Skickar..." - } - } - } - }, "Sent with foreground app connections to this gateway. Changes apply on the next reconnect; Share extension delivery is not yet supported.": { "localizations": { "en": { @@ -158577,6 +157353,142 @@ } } }, + "Shows the full gateway status label": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Shows the full gateway status label" + } + }, + "zh-CN": { + "stringUnit": { + "state": "translated", + "value": "显示完整网关状态标签" + } + }, + "zh-TW": { + "stringUnit": { + "state": "translated", + "value": "顯示完整閘道狀態標籤" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Mostra o rótulo completo de status do gateway" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Zeigt die vollständige Gateway-Statusbezeichnung an" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Muestra la etiqueta completa de estado de la pasarela" + } + }, + "ja-JP": { + "stringUnit": { + "state": "translated", + "value": "ゲートウェイの完全なステータスラベルを表示" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "전체 게이트웨이 상태 레이블 표시" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Affiche le libellé d’état complet de la passerelle" + } + }, + "hi": { + "stringUnit": { + "state": "translated", + "value": "पूरा गेटवे स्थिति लेबल दिखाता है" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "يعرض تسمية حالة البوابة كاملة" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Mostra l’etichetta completa di stato del gateway" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Tam ağ geçidi durum etiketini gösterir" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Показує повну мітку стану шлюзу" + } + }, + "id": { + "stringUnit": { + "state": "translated", + "value": "Tampilkan label status gateway lengkap" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Pokazuje pełną etykietę stanu bramy" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "แสดงป้ายสถานะเกตเวย์แบบเต็ม" + } + }, + "vi": { + "stringUnit": { + "state": "translated", + "value": "Hiện đầy đủ nhãn trạng thái cổng kết nối" + } + }, + "nl": { + "stringUnit": { + "state": "translated", + "value": "Toont het volledige gateway-statuslabel" + } + }, + "fa": { + "stringUnit": { + "state": "translated", + "value": "برچسب کامل وضعیت درگاه را نشان می‌دهد" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Показывает полную метку состояния шлюза" + } + }, + "sv": { + "stringUnit": { + "state": "translated", + "value": "Visar hela gateway-statusetiketten" + } + } + } + }, "Signal Entries": { "localizations": { "en": { @@ -162113,142 +161025,6 @@ } } }, - "Speech language": { - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Speech language" - } - }, - "zh-CN": { - "stringUnit": { - "state": "translated", - "value": "语音语言" - } - }, - "zh-TW": { - "stringUnit": { - "state": "translated", - "value": "語音語言" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Idioma da fala" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Sprache für Spracherkennung" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Idioma de voz" - } - }, - "ja-JP": { - "stringUnit": { - "state": "translated", - "value": "音声言語" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "음성 언어" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Langue vocale" - } - }, - "hi": { - "stringUnit": { - "state": "translated", - "value": "बोलने की भाषा" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "لغة الكلام" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Lingua vocale" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Konuşma dili" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Мова мовлення" - } - }, - "id": { - "stringUnit": { - "state": "translated", - "value": "Bahasa ucapan" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Język mowy" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "ภาษาพูด" - } - }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Ngôn ngữ nói" - } - }, - "nl": { - "stringUnit": { - "state": "translated", - "value": "Spraaktaal" - } - }, - "fa": { - "stringUnit": { - "state": "translated", - "value": "زبان گفتار" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Язык речи" - } - }, - "sv": { - "stringUnit": { - "state": "translated", - "value": "Talspråk" - } - } - } - }, "Speech recognition": { "localizations": { "en": { @@ -176393,142 +175169,6 @@ } } }, - "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider.": { - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider." - } - }, - "zh-CN": { - "stringUnit": { - "state": "new", - "value": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider." - } - }, - "zh-TW": { - "stringUnit": { - "state": "new", - "value": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider." - } - }, - "pt-BR": { - "stringUnit": { - "state": "new", - "value": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider." - } - }, - "de": { - "stringUnit": { - "state": "new", - "value": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider." - } - }, - "es": { - "stringUnit": { - "state": "new", - "value": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider." - } - }, - "ja-JP": { - "stringUnit": { - "state": "new", - "value": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider." - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "이 기기에서 Talk가 실시간 음성을 사용하려면 게이트웨이 승인이 필요합니다. 오디오는 이 기기에서 음성 제공자로 직접 전송됩니다." - } - }, - "fr": { - "stringUnit": { - "state": "new", - "value": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider." - } - }, - "hi": { - "stringUnit": { - "state": "new", - "value": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider." - } - }, - "ar": { - "stringUnit": { - "state": "new", - "value": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider." - } - }, - "it": { - "stringUnit": { - "state": "new", - "value": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider." - } - }, - "tr": { - "stringUnit": { - "state": "new", - "value": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider." - } - }, - "uk": { - "stringUnit": { - "state": "new", - "value": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider." - } - }, - "id": { - "stringUnit": { - "state": "new", - "value": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider." - } - }, - "pl": { - "stringUnit": { - "state": "new", - "value": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider." - } - }, - "th": { - "stringUnit": { - "state": "new", - "value": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider." - } - }, - "vi": { - "stringUnit": { - "state": "new", - "value": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider." - } - }, - "nl": { - "stringUnit": { - "state": "new", - "value": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider." - } - }, - "fa": { - "stringUnit": { - "state": "new", - "value": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider." - } - }, - "ru": { - "stringUnit": { - "state": "new", - "value": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider." - } - }, - "sv": { - "stringUnit": { - "state": "new", - "value": "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider." - } - } - } - }, "This disconnects, clears saved gateway credentials, and reopens onboarding.": { "localizations": { "en": { @@ -178977,6 +177617,142 @@ } } }, + "Transcribe speech into the message": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Transcribe speech into the message" + } + }, + "zh-CN": { + "stringUnit": { + "state": "translated", + "value": "将语音转写到消息中。" + } + }, + "zh-TW": { + "stringUnit": { + "state": "translated", + "value": "將語音轉成訊息文字。" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Transcreva a fala na mensagem." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Sprache in die Nachricht transkribieren." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Transcribe la voz en el mensaje." + } + }, + "ja-JP": { + "stringUnit": { + "state": "translated", + "value": "音声をメッセージに文字起こしします。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "음성을 메시지에 텍스트로 입력합니다." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Transcrire la parole dans le message." + } + }, + "hi": { + "stringUnit": { + "state": "translated", + "value": "बोली को संदेश में लिखें।" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "حوّل الكلام إلى نص داخل الرسالة." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Trascrivi il parlato nel messaggio." + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Konuşmayı mesaja aktar." + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Перетворити мовлення на текст повідомлення." + } + }, + "id": { + "stringUnit": { + "state": "translated", + "value": "Transkripsikan ucapan ke dalam pesan." + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Przepisz mowę do wiadomości." + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ถอดเสียงพูดเป็นข้อความในข้อความ." + } + }, + "vi": { + "stringUnit": { + "state": "translated", + "value": "Chuyển lời nói thành văn bản trong tin nhắn." + } + }, + "nl": { + "stringUnit": { + "state": "translated", + "value": "Zet spraak om naar tekst in het bericht." + } + }, + "fa": { + "stringUnit": { + "state": "translated", + "value": "گفتار را به متن پیام تبدیل کنید." + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Преобразовать речь в текст сообщения." + } + }, + "sv": { + "stringUnit": { + "state": "translated", + "value": "Omvandla tal till text i meddelandet." + } + } + } + }, "Transport": { "localizations": { "en": { @@ -184281,6 +183057,278 @@ } } }, + "Using %@": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Using %@" + } + }, + "zh-CN": { + "stringUnit": { + "state": "translated", + "value": "使用 %@" + } + }, + "zh-TW": { + "stringUnit": { + "state": "translated", + "value": "使用 %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Usando %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Mit %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Usando %@" + } + }, + "ja-JP": { + "stringUnit": { + "state": "translated", + "value": "%@を使用中" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "%@ 사용 중" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Avec %@" + } + }, + "hi": { + "stringUnit": { + "state": "translated", + "value": "%@ का उपयोग करके" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "استخدام %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Con %@" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "%@ kullanılıyor" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "За допомогою %@" + } + }, + "id": { + "stringUnit": { + "state": "translated", + "value": "Menggunakan %@" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Przez %@" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ใช้ %@" + } + }, + "vi": { + "stringUnit": { + "state": "translated", + "value": "Đang dùng %@" + } + }, + "nl": { + "stringUnit": { + "state": "translated", + "value": "Met %@" + } + }, + "fa": { + "stringUnit": { + "state": "translated", + "value": "با استفاده از %@" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "С использованием %@" + } + }, + "sv": { + "stringUnit": { + "state": "translated", + "value": "Med %@" + } + } + } + }, + "Using a tool": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Using a tool" + } + }, + "zh-CN": { + "stringUnit": { + "state": "translated", + "value": "使用工具" + } + }, + "zh-TW": { + "stringUnit": { + "state": "translated", + "value": "使用工具" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Usando uma ferramenta" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Tool verwenden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Usando una herramienta" + } + }, + "ja-JP": { + "stringUnit": { + "state": "translated", + "value": "ツールを使用中" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "도구 사용 중" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Utilisation d’un outil" + } + }, + "hi": { + "stringUnit": { + "state": "translated", + "value": "टूल का उपयोग" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "استخدام أداة" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Uso di uno strumento" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Bir araç kullanılıyor" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "За допомогою інструмента" + } + }, + "id": { + "stringUnit": { + "state": "translated", + "value": "Menggunakan alat" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Korzystanie z narzędzia" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ใช้เครื่องมือ" + } + }, + "vi": { + "stringUnit": { + "state": "translated", + "value": "Đang dùng công cụ" + } + }, + "nl": { + "stringUnit": { + "state": "translated", + "value": "Tool gebruiken" + } + }, + "fa": { + "stringUnit": { + "state": "translated", + "value": "استفاده از ابزار" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "С помощью инструмента" + } + }, + "sv": { + "stringUnit": { + "state": "translated", + "value": "Använder ett verktyg" + } + } + } + }, "Using iOS Speech fallback": { "localizations": { "en": { @@ -186049,142 +185097,6 @@ } } }, - "Voice & Talk Settings": { - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Voice & Talk Settings" - } - }, - "zh-CN": { - "stringUnit": { - "state": "translated", - "value": "语音和通话设置" - } - }, - "zh-TW": { - "stringUnit": { - "state": "translated", - "value": "語音與通話設定" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Configurações de voz e fala" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Einstellungen für Stimme & Sprechen" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Configuración de voz y conversación" - } - }, - "ja-JP": { - "stringUnit": { - "state": "translated", - "value": "音声と会話の設定" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "음성 및 대화 설정" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Paramètres de voix et de conversation" - } - }, - "hi": { - "stringUnit": { - "state": "translated", - "value": "वॉइस और बातचीत सेटिंग्स" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "إعدادات الصوت والتحدث" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Impostazioni Voce e conversazione" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Ses ve Konuşma Ayarları" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Налаштування голосу й розмови" - } - }, - "id": { - "stringUnit": { - "state": "translated", - "value": "Pengaturan Suara & Bicara" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Ustawienia głosu i rozmowy" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "การตั้งค่าเสียงและการพูด" - } - }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cài đặt Giọng nói & Trò chuyện" - } - }, - "nl": { - "stringUnit": { - "state": "translated", - "value": "Instellingen voor spraak en praten" - } - }, - "fa": { - "stringUnit": { - "state": "translated", - "value": "تنظیمات صدا و گفت‌وگو" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Настройки голоса и разговора" - } - }, - "sv": { - "stringUnit": { - "state": "translated", - "value": "Inställningar för röst och samtal" - } - } - } - }, "Voice Mode": { "localizations": { "en": { @@ -186729,138 +185641,138 @@ } } }, - "Voice is disabled in Apple Review demo mode.": { + "Voice active": { "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Voice is disabled in Apple Review demo mode." + "value": "Voice active" } }, "zh-CN": { "stringUnit": { "state": "translated", - "value": "Apple 审核演示模式下已禁用语音。" + "value": "语音已开启" } }, "zh-TW": { "stringUnit": { "state": "translated", - "value": "Apple 審查展示模式已停用語音功能。" + "value": "語音已開啟" } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "A voz está desativada no modo de demonstração para a revisão da Apple." + "value": "Voz ativa" } }, "de": { "stringUnit": { "state": "translated", - "value": "Sprache ist im Apple Review-Demomodus deaktiviert." + "value": "Sprache aktiv" } }, "es": { "stringUnit": { "state": "translated", - "value": "La voz está desactivada en el modo de demostración de Apple Review." + "value": "Voz activada" } }, "ja-JP": { "stringUnit": { "state": "translated", - "value": "Apple Reviewデモモードでは音声が無効になっています。" + "value": "音声オン" } }, "ko": { "stringUnit": { "state": "translated", - "value": "Apple Review 데모 모드에서는 음성이 비활성화됩니다." + "value": "음성 켜짐" } }, "fr": { "stringUnit": { "state": "translated", - "value": "La voix est désactivée en mode démo Apple Review." + "value": "Voix activée" } }, "hi": { "stringUnit": { "state": "translated", - "value": "Apple Review डेमो मोड में वॉइस अक्षम है।" + "value": "आवाज़ चालू" } }, "ar": { "stringUnit": { "state": "translated", - "value": "الصوت معطّل في وضع العرض التوضيحي لمراجعة Apple." + "value": "الصوت مفعّل" } }, "it": { "stringUnit": { "state": "translated", - "value": "La voce è disabilitata nella modalità demo per la revisione di Apple." + "value": "Voce attiva" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Apple Review demo modunda ses devre dışıdır." + "value": "Ses açık" } }, "uk": { "stringUnit": { "state": "translated", - "value": "Голос вимкнено в демонстраційному режимі Apple Review." + "value": "Голос увімкнено" } }, "id": { "stringUnit": { "state": "translated", - "value": "Suara dinonaktifkan dalam mode demo Apple Review." + "value": "Suara aktif" } }, "pl": { "stringUnit": { "state": "translated", - "value": "Obsługa głosu jest wyłączona w trybie demonstracyjnym Apple Review." + "value": "Głos włączony" } }, "th": { "stringUnit": { "state": "translated", - "value": "เสียงถูกปิดใช้งานในโหมดสาธิตสำหรับ Apple Review" + "value": "เสียงเปิดอยู่" } }, "vi": { "stringUnit": { "state": "translated", - "value": "Tính năng giọng nói bị tắt trong chế độ demo Apple Review." + "value": "Giọng nói đang bật" } }, "nl": { "stringUnit": { "state": "translated", - "value": "Spraak is uitgeschakeld in de Apple Review-demomodus." + "value": "Stem actief" } }, "fa": { "stringUnit": { "state": "translated", - "value": "صدا در حالت دموی Apple Review غیرفعال است." + "value": "صدا فعال است" } }, "ru": { "stringUnit": { "state": "translated", - "value": "Голосовые функции отключены в демонстрационном режиме Apple Review." + "value": "Голос включён" } }, "sv": { "stringUnit": { "state": "translated", - "value": "Röst är inaktiverat i demoläget för Apple Review." + "value": "Röst aktiv" } } } @@ -187137,138 +186049,138 @@ } } }, - "Waiting for Approval": { + "Voice off": { "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Waiting for Approval" + "value": "Voice off" } }, "zh-CN": { "stringUnit": { "state": "translated", - "value": "等待批准" + "value": "语音已关闭" } }, "zh-TW": { "stringUnit": { "state": "translated", - "value": "等待核准" + "value": "語音已關閉" } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "Aguardando aprovação" + "value": "Voz desativada" } }, "de": { "stringUnit": { "state": "translated", - "value": "Warten auf Genehmigung" + "value": "Sprache aus" } }, "es": { "stringUnit": { "state": "translated", - "value": "Esperando aprobación" + "value": "Voz desactivada" } }, "ja-JP": { "stringUnit": { "state": "translated", - "value": "承認待ち" + "value": "音声オフ" } }, "ko": { "stringUnit": { "state": "translated", - "value": "승인 대기 중" + "value": "음성 꺼짐" } }, "fr": { "stringUnit": { "state": "translated", - "value": "En attente d’approbation" + "value": "Voix désactivée" } }, "hi": { "stringUnit": { "state": "translated", - "value": "स्वीकृति की प्रतीक्षा है" + "value": "आवाज़ बंद" } }, "ar": { "stringUnit": { "state": "translated", - "value": "في انتظار الموافقة" + "value": "الصوت متوقف" } }, "it": { "stringUnit": { "state": "translated", - "value": "In attesa di approvazione" + "value": "Voce disattivata" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Onay Bekleniyor" + "value": "Ses kapalı" } }, "uk": { "stringUnit": { "state": "translated", - "value": "Очікування схвалення" + "value": "Голос вимкнено" } }, "id": { "stringUnit": { "state": "translated", - "value": "Menunggu Persetujuan" + "value": "Suara mati" } }, "pl": { "stringUnit": { "state": "translated", - "value": "Oczekiwanie na zatwierdzenie" + "value": "Głos wyłączony" } }, "th": { "stringUnit": { "state": "translated", - "value": "กำลังรอการอนุมัติ" + "value": "เสียงปิดอยู่" } }, "vi": { "stringUnit": { "state": "translated", - "value": "Đang chờ phê duyệt" + "value": "Giọng nói đang tắt" } }, "nl": { "stringUnit": { "state": "translated", - "value": "Wachten op goedkeuring" + "value": "Stem uit" } }, "fa": { "stringUnit": { "state": "translated", - "value": "در انتظار تأیید" + "value": "صدا خاموش است" } }, "ru": { "stringUnit": { "state": "translated", - "value": "Ожидает подтверждения" + "value": "Голос выключен" } }, "sv": { "stringUnit": { "state": "translated", - "value": "Väntar på godkännande" + "value": "Röst av" } } } diff --git a/apps/ios/Sources/Design/ChatProTab.swift b/apps/ios/Sources/Design/ChatProTab.swift index 595a95985f20..6acba48669ff 100644 --- a/apps/ios/Sources/Design/ChatProTab.swift +++ b/apps/ios/Sources/Design/ChatProTab.swift @@ -15,6 +15,12 @@ private struct ChatScrollEdgeTreatment: ViewModifier { } struct ChatProTab: View { + enum GatewayStatusTone: Equatable { + case success + case warning + case error + } + private struct TranscriptShareItem: Identifiable { let id = UUID() let fileURL: URL @@ -38,6 +44,7 @@ struct ChatProTab: View { @State private var viewModelPresentationAgentBadge = "M" @State private var viewModelHasVerifiedOfflineRoutingIdentity = false @State private var speech: OpenClawChatSpeechController? + @State private var isGatewayStatusManuallyExpanded = false let headerLeadingAction: OpenClawSidebarHeaderAction? let headerTitle: String? let showsAgentBadge: Bool @@ -121,7 +128,7 @@ struct ChatProTab: View { private var content: some View { self.chatSurface .modifier(ChatScrollEdgeTreatment()) - .navigationTitle(self.headerDisplayTitle) + .navigationTitle(self.showsAgentBadge ? "" : self.headerDisplayTitle) .navigationBarTitleDisplayMode(.inline) .toolbar { if let headerLeadingAction { @@ -130,17 +137,24 @@ struct ChatProTab: View { } } if self.showsAgentBadge { - ToolbarItem(placement: .topBarLeading) { - self.headerIdentityBadge + if #available(iOS 26.0, *) { + ToolbarItem(placement: .topBarLeading) { + self.headerAgentIdentity + } + .sharedBackgroundVisibility(.hidden) + } else { + ToolbarItem(placement: .topBarLeading) { + self.headerAgentIdentity + } + } + } else { + ToolbarItem(placement: .topBarTrailing) { + self.headerGatewayStatus } } ToolbarItem(placement: .topBarTrailing) { self.chatActionsMenu } - ToolbarItem(placement: .topBarTrailing) { - self.connectionStatusButton - .accessibilityIdentifier("chat-gateway-status") - } } .sheet(item: self.$transcriptShareItem) { item in ChatTranscriptShareSheet(fileURL: item.fileURL) @@ -189,6 +203,7 @@ struct ChatProTab: View { emptyAssistantIntro: String(localized: "What would you like to work on?"), emptyAssistantPrompts: Self.emptyAssistantPrompts, talkControl: viewModel.isAttachmentOwnerPinned ? nil : self.talkControl, + dictationControl: viewModel.isAttachmentOwnerPinned ? nil : self.dictationControl, voiceNoteControl: self.voiceNoteControl, speech: self.speech) // iMessage-style grey bubbles for agent replies in the clean chrome. @@ -204,16 +219,184 @@ struct ChatProTab: View { } } - /// Flat circular avatar for the nav bar — no gradient/shadow, per Apple bar-button sizing. + /// Voice activity owns the contour while gateway state stays a compact, + /// stable dot on the avatar instead of competing with it in the toolbar. private var headerIdentityBadge: some View { - Text(self.agentBadge) - .font(OpenClawType.avatar(size: self.agentBadge.count > 2 ? 12 : 15)) - .foregroundStyle(.white) - .minimumScaleFactor(0.6) + TalkAvatarWaveformView( + phase: self.voiceAvatarPhase, + palette: .openClawBrand, + diameter: 38, + avatarDiameter: 28) + { + Text(self.agentBadge) + .font(OpenClawType.avatar(size: self.agentBadge.count > 2 ? 12 : 16)) + .foregroundStyle(.white) + .minimumScaleFactor(0.6) + .lineLimit(1) + .frame(width: 28, height: 28) + .background(Circle().fill(OpenClawBrand.carapaceElevated)) + } + .overlay(alignment: .topTrailing) { + self.gatewayAvatarStatusDot + } + .accessibilityElement(children: .ignore) + } + + private var gatewayAvatarStatusDot: some View { + ZStack { + Circle() + .fill(Color(uiColor: .systemBackground)) + Circle() + .fill(self.gatewayStatusColor) + .padding(2) + } + .frame(width: 13, height: 13) + .accessibilityHidden(true) + } + + private var headerAgentIdentity: some View { + HStack { + self.headerAgentIdentityControl + } + .frame(minHeight: 44) + .accessibilityIdentifier("chat-agent-identity") + .animation(.snappy(duration: 0.24), value: self.showsExpandedGatewayStatus) + } + + @ViewBuilder + private var headerGatewayStatus: some View { + if self.gatewayStatusIsHealthy || self.openSettings != nil { + Button(action: self.handleHeaderAgentIdentityTap) { + self.headerGatewayStatusLabel + } + .buttonStyle(.plain) + .accessibilityLabel(Text(verbatim: self.gatewayAccessibilityLabel)) + .accessibilityHint(self.gatewayStatusAccessibilityHint) + .accessibilityIdentifier("chat-gateway-status") + } else { + self.headerGatewayStatusLabel + .accessibilityElement(children: .ignore) + .accessibilityLabel(Text(verbatim: self.gatewayAccessibilityLabel)) + .accessibilityIdentifier("chat-gateway-status") + } + } + + private var headerGatewayStatusLabel: some View { + HStack(spacing: 6) { + Circle() + .fill(self.gatewayStatusColor) + .frame(width: 10, height: 10) + if self.showsExpandedGatewayStatus { + self.expandedGatewayStatusLabel + } + } + .frame(minHeight: 44) + .animation(.snappy(duration: 0.24), value: self.showsExpandedGatewayStatus) + } + + @ViewBuilder + private var headerAgentIdentityControl: some View { + if self.gatewayStatusIsHealthy || self.openSettings != nil { + Button(action: self.handleHeaderAgentIdentityTap) { + self.headerAgentIdentityLabel + } + .buttonStyle(.plain) + .accessibilityLabel(Text(verbatim: self.headerAgentAccessibilityLabel)) + .accessibilityValue(self.showsExpandedGatewayStatus ? "Expanded" : "Collapsed") + .accessibilityHint(self.gatewayStatusAccessibilityHint) + .accessibilityIdentifier("chat-gateway-status") + } else { + self.headerAgentIdentityLabel + .accessibilityElement(children: .ignore) + .accessibilityLabel(Text(verbatim: self.headerAgentAccessibilityLabel)) + .accessibilityIdentifier("chat-gateway-status") + } + } + + private var headerAgentIdentityLabel: some View { + HStack(spacing: 7) { + self.headerIdentityBadge + if self.showsExpandedGatewayStatus { + self.expandedGatewayStatusLabel + .transition(.opacity.combined(with: .move(edge: .leading))) + } else { + Text(self.agentDisplayName) + .font(OpenClawType.headline) + .lineLimit(1) + .transition(.opacity) + } + } + } + + private var gatewayStatusIsHealthy: Bool { + Self.gatewayStatusTone( + state: self.gatewayDisplayState, + isGatewayUsable: self.gatewayConnected) == .success + } + + private var headerAgentAccessibilityLabel: String { + "\(self.voiceAvatarAccessibilityLabel). \(self.gatewayAccessibilityLabel)" + } + + private func handleHeaderAgentIdentityTap() { + if self.gatewayStatusIsHealthy { + withAnimation(.snappy(duration: 0.24)) { + self.isGatewayStatusManuallyExpanded.toggle() + } + } else { + self.openSettings?() + } + } + + private var expandedGatewayStatusLabel: some View { + Text(Self.gatewayStatusTitle(state: self.gatewayDisplayState, isGatewayUsable: self.gatewayConnected)) + .font(OpenClawType.subheadMedium) + .foregroundStyle(.primary) .lineLimit(1) - .frame(width: 30, height: 30) - .background(Circle().fill(OpenClawBrand.accent)) - .accessibilityLabel(self.agentDisplayName) + .fixedSize(horizontal: true, vertical: false) + .padding(.horizontal, 9) + .padding(.vertical, 5) + .background(self.gatewayStatusColor.opacity(0.12), in: Capsule()) + .overlay { + Capsule() + .stroke(self.gatewayStatusColor.opacity(0.28), lineWidth: 1) + } + } + + private var showsExpandedGatewayStatus: Bool { + Self.gatewayStatusShouldExpand( + state: self.gatewayDisplayState, + isGatewayUsable: self.gatewayConnected, + isManuallyExpanded: self.isGatewayStatusManuallyExpanded) + } + + private var gatewayStatusAccessibilityHint: String { + if !self.gatewayStatusIsHealthy { + return String(localized: "Opens Settings / Gateway") + } + return self.isGatewayStatusManuallyExpanded + ? String(localized: "Hides the gateway status label") + : String(localized: "Shows the full gateway status label") + } + + private var voiceAvatarPhase: TalkWaveformPhase { + guard self.appModel.talkMode.isEnabled else { return .idle } + if self.appModel.talkMode.isSpeaking { + return .speaking(level: self.appModel.talkMode.playbackLevel) + } + if self.appModel.talkMode.isListening { + return .listening( + level: self.appModel.talkMode.micLevel, + speechActive: self.appModel.talkMode.isUserSpeechDetected) + } + return .idle + } + + private var voiceAvatarAccessibilityLabel: String { + let state = self.appModel.talkMode.isEnabled + ? self.appModel.talkMode.statusText + : String(localized: "Voice off") + return "\(self.agentDisplayName), \(state)" } private func syncChatViewModel() { @@ -241,6 +424,7 @@ struct ChatProTab: View { // Keep recording, staging, and delivery on their captured route. // The pin-change observer replays this rebuild with latest state. guard !viewModel.isAttachmentOwnerPinned else { return } + viewModel.endPendingToolActivities() self.viewModelOwnerID = ownerID self.viewModelTransportAgentID = transportAgentID self.viewModelRoutingContract = routingContract @@ -283,6 +467,18 @@ struct ChatProTab: View { onSessionChanged: { sessionKey in self.appModel.focusChatSession(sessionKey) }, + onToolActivity: { id, name, isActive, toolSessionKey in + if isActive { + LiveActivityManager.shared.showTool( + id: id, + name: name, + agentName: self.agentDisplayName, + agentBadge: self.agentBadge, + sessionKey: toolSessionKey) + } else { + LiveActivityManager.shared.endTool(id: id, sessionKey: toolSessionKey) + } + }, diagnosticsLog: { message in GatewayDiagnostics.log(message) }) @@ -303,6 +499,21 @@ struct ChatProTab: View { }) } + private var dictationControl: OpenClawChatDictationControl { + OpenClawChatDictationControl( + isActive: self.appModel.isChatDictationActive, + isAvailable: !self.appModel.isTalkCaptureActive || self.appModel.isChatDictationActive, + start: { + try await self.appModel.transcribeChatDraft() + }, + finish: { + self.appModel.finishChatDictation() + }, + cancel: { + self.appModel.cancelChatDictation() + }) + } + private var talkLevel: Double { if self.appModel.talkMode.isSpeaking { return self.appModel.talkMode.playbackLevel ?? 0 @@ -319,33 +530,6 @@ struct ChatProTab: View { isTalkActive: self.appModel.isTalkCaptureActive) } - @ViewBuilder - private var connectionStatusButton: some View { - if let openSettings { - Button(action: openSettings) { - self.connectionPill - } - .buttonStyle(.plain) - .accessibilityLabel(self.gatewayAccessibilityLabel) - .accessibilityHint("Opens Settings / Gateway") - } else { - self.connectionPill - .accessibilityLabel(self.gatewayAccessibilityLabel) - } - } - - private var connectionPill: some View { - HStack(spacing: 5) { - ProStatusDot(color: self.gatewayPillColor) - Text(Self.gatewayPillTitle(state: self.gatewayDisplayState, isGatewayUsable: self.gatewayConnected)) - .font(OpenClawType.subheadMedium) - .lineLimit(1) - } - .foregroundStyle(self.gatewayPillColor) - // Even breathing room inside the system glass capsule. - .padding(.horizontal, 6) - } - private var chatActionsMenu: some View { Menu { Button { @@ -411,6 +595,20 @@ struct ChatProTab: View { } .disabled(self.viewModel == nil) + if let openSettings { + Divider() + + Button(action: openSettings) { + Label { + Text("Gateway Settings") + .font(OpenClawType.body) + } icon: { + Image(systemName: "network") + } + } + .accessibilityIdentifier("chat-gateway-settings") + } + Toggle(isOn: self.$showsAssistantTrace) { Label { Text(String(localized: "Show reasoning & tool activity")) @@ -472,23 +670,48 @@ struct ChatProTab: View { } private var gatewayAccessibilityLabel: String { - "Gateway: \(Self.gatewayPillTitle(state: self.gatewayDisplayState, isGatewayUsable: self.gatewayConnected))" + "Gateway: \(Self.gatewayStatusTitle(state: self.gatewayDisplayState, isGatewayUsable: self.gatewayConnected))" } - private var gatewayPillColor: Color { - switch self.gatewayDisplayState { - case .connected: - self.gatewayConnected ? OpenClawBrand.ok : .secondary - case .connecting: - OpenClawBrand.accent + private var gatewayStatusColor: Color { + switch Self.gatewayStatusTone( + state: self.gatewayDisplayState, + isGatewayUsable: self.gatewayConnected) + { + case .success: + OpenClawBrand.statusSuccess + case .warning: + OpenClawBrand.statusWarning case .error: - OpenClawBrand.warn - case .disconnected: - .secondary + OpenClawBrand.statusError } } - nonisolated static func gatewayPillTitle(state: GatewayDisplayState, isGatewayUsable: Bool) -> String { + nonisolated static func gatewayStatusTone( + state: GatewayDisplayState, + isGatewayUsable: Bool) -> GatewayStatusTone + { + switch state { + case .connected: + isGatewayUsable ? .success : .warning + case .connecting, .error: + .warning + case .disconnected: + .error + } + } + + nonisolated static func gatewayStatusShouldExpand( + state: GatewayDisplayState, + isGatewayUsable: Bool, + isManuallyExpanded: Bool) -> Bool + { + isManuallyExpanded || self.gatewayStatusTone( + state: state, + isGatewayUsable: isGatewayUsable) != .success + } + + nonisolated static func gatewayStatusTitle(state: GatewayDisplayState, isGatewayUsable: Bool) -> String { switch state { case .connected: isGatewayUsable ? "Connected" : "Unavailable" @@ -564,7 +787,7 @@ struct ChatProTab: View { } private var currentAgentBadge: String { - if let identity = self.currentActiveAgent?.identity, + if let identity = currentActiveAgent?.identity, let emoji = identity["emoji"]?.value as? String, let normalizedEmoji = Self.normalizedBadgeEmoji(emoji) { @@ -578,20 +801,11 @@ struct ChatProTab: View { } nonisolated static func initialsBadge(for displayName: String) -> String { - let words = displayName - .split(whereSeparator: { $0.isWhitespace || $0 == "-" || $0 == "_" }) - .prefix(2) - let initials = words.compactMap(\.first).map(String.init).joined() - if !initials.isEmpty { - return initials.uppercased() - } - return "OC" + AgentIdentityPresentation.initialsBadge(for: displayName) } nonisolated static func normalizedBadgeEmoji(_ value: String?) -> String? { - guard let value else { return nil } - let normalized = value.trimmingCharacters(in: .whitespacesAndNewlines) - return normalized.isEmpty || normalized == "?" ? nil : normalized + AgentIdentityPresentation.normalizedBadgeEmoji(value) } nonisolated static func transportAgentID(_ value: String?) -> String { diff --git a/apps/ios/Sources/Design/OpenClawBrand.swift b/apps/ios/Sources/Design/OpenClawBrand.swift index ef482091da85..9ca61d980cf8 100644 --- a/apps/ios/Sources/Design/OpenClawBrand.swift +++ b/apps/ios/Sources/Design/OpenClawBrand.swift @@ -74,6 +74,11 @@ final class AppAppearanceModel { } enum OpenClawBrand { + // Carapace semantic palette: these tokens are shared by voice surfaces that + // need the web system's quieter ink/paper hierarchy in native SwiftUI. + static let carapaceElevated = Color(red: 32 / 255.0, green: 32 / 255.0, blue: 36 / 255.0) + static let carapaceCoral = Color(red: 245 / 255.0, green: 101 / 255.0, blue: 74 / 255.0) + static let carapaceSea = Color(red: 79 / 255.0, green: 200 / 255.0, blue: 174 / 255.0) // Accent fills stay dark enough for white content; foreground accents adapt // separately so small labels retain 4.5:1 contrast on dark surfaces and tinted pills. static let uiAccent = adaptiveUIColor(light: (183, 56, 51), dark: (198, 62, 56)) @@ -88,6 +93,11 @@ enum OpenClawBrand { static let uiWarn = adaptiveUIColor(light: (154, 87, 0), dark: (255, 214, 10)) static let uiDanger = adaptiveUIColor(light: (185, 28, 28), dark: (252, 165, 165)) static let uiInfo = adaptiveUIColor(light: (0, 91, 196), dark: (100, 168, 255)) + // Carapace status foreground tokens. Keep these separate from the broader + // app feedback palette so compact state indicators match the web system. + static let uiStatusSuccess = adaptiveUIColor(light: (22, 163, 74), dark: (34, 197, 94)) + static let uiStatusWarning = adaptiveUIColor(light: (217, 119, 6), dark: (251, 191, 36)) + static let uiStatusError = adaptiveUIColor(light: (220, 38, 38), dark: (239, 68, 68)) static let accent = Color(uiColor: Self.uiAccent) static let accentForeground = Color(uiColor: Self.uiAccentForeground) @@ -101,6 +111,9 @@ enum OpenClawBrand { static let ok = Color(uiColor: Self.uiOK) static let warn = Color(uiColor: Self.uiWarn) static let info = Color(uiColor: Self.uiInfo) + static let statusSuccess = Color(uiColor: Self.uiStatusSuccess) + static let statusWarning = Color(uiColor: Self.uiStatusWarning) + static let statusError = Color(uiColor: Self.uiStatusError) static let graphite = void static let graphiteElevated = obsidian static let activationCanvas = Color(uiColor: adaptiveUIColor(light: (255, 255, 255), dark: (18, 14, 15))) @@ -208,6 +221,22 @@ enum OpenClawBrand { } } +extension TalkWaveformPalette { + /// iOS app branding for shared voice contours. This lives with the palette + /// owner so removing a presentation surface cannot remove the app theme. + static let openClawBrand = TalkWaveformPalette( + active: [ + OpenClawBrand.carapaceCoral, + OpenClawBrand.carapaceSea, + OpenClawBrand.accent, + ], + inactive: [ + Color(uiColor: .systemGray2), + Color(uiColor: .systemGray3), + Color(uiColor: .systemGray4), + ]) +} + struct OpenClawActivationGlyph: View { let size: CGFloat var mood: OpenClawMascotMood = .idle diff --git a/apps/ios/Sources/Design/OpenClawProComponents.swift b/apps/ios/Sources/Design/OpenClawProComponents.swift index 22ee5bc16a79..6e2f239936c9 100644 --- a/apps/ios/Sources/Design/OpenClawProComponents.swift +++ b/apps/ios/Sources/Design/OpenClawProComponents.swift @@ -584,16 +584,6 @@ struct OpenClawStatusBadge: View { } } -struct ProStatusDot: View { - var color: Color - - var body: some View { - Circle() - .fill(self.color) - .frame(width: 8, height: 8) - } -} - struct ProValuePill: View { @Environment(\.colorScheme) private var colorScheme let value: String diff --git a/apps/ios/Sources/Design/RootTabsPhoneChrome.swift b/apps/ios/Sources/Design/RootTabsPhoneChrome.swift new file mode 100644 index 000000000000..b35baa189678 --- /dev/null +++ b/apps/ios/Sources/Design/RootTabsPhoneChrome.swift @@ -0,0 +1,114 @@ +import SwiftUI +import UIKit + +enum UnifiedChatVoiceTabIcon { + enum State: Hashable { + case inactive + case active + case listening + case speaking + + var isVoiceActive: Bool { + self != .inactive + } + } + + struct CacheKey: Hashable { + let state: State + let colorScheme: ColorScheme + } + + private static let canvasSize = CGSize(width: 30, height: 30) + @MainActor private static var imageCache: [CacheKey: UIImage] = [:] + + @MainActor + static func image(state: State, colorScheme: ColorScheme) -> Image { + // Voice-active icons render the adaptive brand color into bitmap pixels; + // appearance must therefore participate in cache identity. + let cacheKey = CacheKey(state: state, colorScheme: colorScheme) + let rendered = self.imageCache[cacheKey] ?? { + let image = self.renderedImage(state: state, colorScheme: colorScheme) + self.imageCache[cacheKey] = image + return image + }() + return Image(uiImage: rendered) + .renderingMode(state.isVoiceActive ? .original : .template) + } + + @MainActor + private static func renderedImage(state: State, colorScheme: ColorScheme) -> UIImage { + let userInterfaceStyle: UIUserInterfaceStyle = colorScheme == .dark ? .dark : .light + let traits = UITraitCollection(userInterfaceStyle: userInterfaceStyle) + let format = UIGraphicsImageRendererFormat(for: traits) + format.opaque = false + format.scale = UIScreen.main.scale + let renderer = UIGraphicsImageRenderer(size: self.canvasSize, format: format) + let rendered = renderer.image { context in + let symbolConfig = UIImage.SymbolConfiguration(pointSize: 25, weight: .regular) + guard let bubble = UIImage(systemName: "bubble.left.fill", withConfiguration: symbolConfig) else { + return + } + let fill = state.isVoiceActive + ? UIColor(OpenClawBrand.accent).resolvedColor(with: traits) + : UIColor.black + let tintedBubble = bubble.withTintColor(fill, renderingMode: .alwaysOriginal) + let bubbleRect = CGRect( + x: (self.canvasSize.width - tintedBubble.size.width) / 2, + y: (self.canvasSize.height - tintedBubble.size.height) / 2, + width: tintedBubble.size.width, + height: tintedBubble.size.height) + tintedBubble.draw(in: bubbleRect) + + // Cut one waveform out of the bubble so the tab keeps a single, + // legible template silhouette at native tab-bar sizes. + context.cgContext.setBlendMode(.clear) + let heights: [CGFloat] = switch state { + case .inactive: [6, 11, 7] + case .active: [7, 12, 8] + case .listening: [9, 15, 10] + case .speaking: [13, 8, 14] + } + for index in heights.indices { + let height = heights[index] + let rect = CGRect( + x: 10.1 + CGFloat(index) * 4.1, + y: 13.4 - height / 2, + width: 2.35, + height: height) + UIBezierPath(roundedRect: rect, cornerRadius: 1.2).fill() + } + } + return rendered.withRenderingMode(state.isVoiceActive ? .alwaysOriginal : .alwaysTemplate) + } +} + +/// Phone tabs push Settings routes (gateway, voice) onto their own stack so +/// Back returns to the tab content the user navigated from; only global flows +/// (deep links, onboarding, problem banner) jump to the canonical Settings tab. +struct PhoneTabSettingsHost: View { + @State private var settingsPath: [SettingsRoute] = [] + private let resetRequestID: Int + private let content: (_ openSettingsRoute: @escaping (SettingsRoute) -> Void) -> Content + + init( + resetRequestID: Int = 0, + @ViewBuilder content: @escaping (_ openSettingsRoute: @escaping (SettingsRoute) -> Void) -> Content) + { + self.resetRequestID = resetRequestID + self.content = content + } + + var body: some View { + NavigationStack(path: self.$settingsPath) { + self.content { route in + self.settingsPath.append(route) + } + .navigationDestination(for: SettingsRoute.self) { route in + SettingsProTab(directRoute: route) + } + } + .onChange(of: self.resetRequestID) { _, _ in + self.settingsPath.removeAll() + } + } +} diff --git a/apps/ios/Sources/Design/RootTabsPhoneControlHub.swift b/apps/ios/Sources/Design/RootTabsPhoneControlHub.swift index 618c83d4c1b5..21f4204cf7de 100644 --- a/apps/ios/Sources/Design/RootTabsPhoneControlHub.swift +++ b/apps/ios/Sources/Design/RootTabsPhoneControlHub.swift @@ -25,7 +25,7 @@ struct RootTabsPhoneControlHub: View { } Section { - self.chatTalkRow + self.chatShortcut .listRowInsets(EdgeInsets(top: 4, leading: 0, bottom: 4, trailing: 0)) .listRowBackground(Color.clear) .listRowSeparator(.hidden) @@ -91,17 +91,10 @@ struct RootTabsPhoneControlHub: View { .accessibilityHint("Opens Settings / Gateway") } - private var chatTalkRow: some View { - // Chat and Talk intentionally stay as Control shortcuts even though they own root tabs. - // These are the hub's primary actions; the remaining destination list filters root tabs. - HStack(alignment: .top, spacing: 12) { - self.prominentDestinationCard( - .chat, - subtitle: "Agent chat and recent work.") - self.prominentDestinationCard( - .talk, - subtitle: "Realtime voice and controls.") - } + private var chatShortcut: some View { + self.prominentDestinationCard( + .chat, + subtitle: "Agent chat and recent work.") } private func prominentDestinationCard( @@ -109,7 +102,7 @@ struct RootTabsPhoneControlHub: View { subtitle: LocalizedStringKey) -> some View { Button { - self.openPhoneRootDestination(destination) + self.applyDestination(destination) } label: { ProCard(padding: 16, radius: OpenClawProMetric.cardRadius) { VStack(alignment: .leading, spacing: 12) { @@ -162,7 +155,7 @@ struct RootTabsPhoneControlHub: View { @ViewBuilder private func detail(for destination: RootTabs.SidebarDestination) -> some View { switch destination { - case .chat, .talk, .agents: + case .chat, .agents: EmptyView() case .gateway: SettingsProTab(directRoute: .gateway) @@ -227,7 +220,7 @@ struct RootTabsPhoneControlHub: View { usesNativeNavigationChrome: true, gatewayAction: { self.openGatewayDetail() }) case .settings: - EmptyView() + SettingsProTab(directRoute: .voice) } } @@ -344,7 +337,7 @@ struct RootTabsPhoneControlHub: View { switch destination { case .chat: OpenClawBrand.ok - case .talk, .skillWorkshop, .files: + case .skillWorkshop, .files: OpenClawBrand.info case .overview: OpenClawBrand.warn diff --git a/apps/ios/Sources/Design/SettingsProTabActions.swift b/apps/ios/Sources/Design/SettingsProTabActions.swift index 95016984a1b1..f2eb2b1a304f 100644 --- a/apps/ios/Sources/Design/SettingsProTabActions.swift +++ b/apps/ios/Sources/Design/SettingsProTabActions.swift @@ -362,6 +362,7 @@ extension SettingsProTab { let link = await self.gatewayController.selectReachableSetupLink(parsedLink) guard self.setupAttemptID == attemptID else { return false } self.stagedGatewaySetupLink = nil + self.setupCode = "" await self.applyGatewayLink(link) return true } diff --git a/apps/ios/Sources/Design/TalkProTab.swift b/apps/ios/Sources/Design/TalkProTab.swift deleted file mode 100644 index 783027f4ae77..000000000000 --- a/apps/ios/Sources/Design/TalkProTab.swift +++ /dev/null @@ -1,484 +0,0 @@ -import OpenClawChatUI -import SwiftUI - -struct TalkProTab: View { - @Environment(NodeAppModel.self) private var appModel - @AppStorage("talk.enabled") private var talkEnabled: Bool = false - @AppStorage(TalkSpeechLocale.storageKey) private var talkSpeechLocale: String = TalkSpeechLocale.automaticID - @AppStorage(TalkDefaults.speakerphoneEnabledKey) private var talkSpeakerphoneEnabled: Bool = - TalkDefaults.speakerphoneEnabledByDefault - @AppStorage("talk.background.enabled") private var talkBackgroundEnabled: Bool = false - @State private var showPermissionPrompt = false - @State private var showTalkIssueDetails = false - let headerLeadingAction: OpenClawSidebarHeaderAction? - let ownsNavigationStack: Bool - var openSettings: () -> Void - var openVoiceSettings: () -> Void - - init( - headerLeadingAction: OpenClawSidebarHeaderAction? = nil, - ownsNavigationStack: Bool = true, - openSettings: @escaping () -> Void, - openVoiceSettings: (() -> Void)? = nil) - { - self.headerLeadingAction = headerLeadingAction - self.ownsNavigationStack = ownsNavigationStack - self.openSettings = openSettings - self.openVoiceSettings = openVoiceSettings ?? openSettings - } - - private var state: TalkProState { - TalkProState( - gatewayConnected: self.gatewayConnected, - isDemoMode: self.appModel.isAppleReviewDemoModeEnabled, - isEnabled: self.appModel.talkMode.isEnabled || self.talkEnabled, - phase: self.appModel.talkMode.phase, - isConfigLoaded: self.appModel.talkMode.gatewayTalkConfigLoaded, - isListening: self.appModel.talkMode.isListening, - isSpeaking: self.appModel.talkMode.isSpeaking, - isUserSpeechDetected: self.appModel.talkMode.isUserSpeechDetected, - permissionState: self.appModel.talkMode.gatewayTalkPermissionState) - } - - var body: some View { - Group { - if self.ownsNavigationStack { - NavigationStack { - self.content - } - } else { - self.content - } - } - .sheet(isPresented: self.$showPermissionPrompt) { - NavigationStack { - TalkPermissionPromptView( - style: .sheet, - onPermissionReady: { - self.showPermissionPrompt = false - self.startTalk() - }) - .padding() - .navigationTitle("Enable Talk") - .toolbar { - ToolbarItem(placement: .cancellationAction) { - Button { - self.showPermissionPrompt = false - } label: { - Text("Not Now") - .font(OpenClawType.subheadSemiBold) - } - } - } - } - .presentationDetents([.medium, .large]) - .openClawSheetChrome() - } - .sheet(isPresented: self.$showTalkIssueDetails) { - if let fallbackIssue = self.fallbackIssue { - TalkRuntimeIssueDetailsSheet( - issue: fallbackIssue, - onOpenSettings: self.openVoiceSettings) - .openClawSheetChrome() - } - } - .onAppear { self.alignPersistedTalkState() } - } - - private var content: some View { - List { - if let fallbackIssue = self.fallbackIssue { - Section { - TalkRuntimeIssueBanner( - issue: fallbackIssue, - onOpenSettings: self.openVoiceSettings, - onShowDetails: { - self.showTalkIssueDetails = true - }) - } - .listRowInsets(EdgeInsets()) - .listRowBackground(Color.clear) - } - self.heroSection - self.conversationSection - self.voiceModeSection - self.controlsSection - } - .navigationTitle("Talk") - .toolbar { - if let headerLeadingAction { - ToolbarItem(placement: .topBarLeading) { - OpenClawSidebarRevealButton(action: headerLeadingAction) - } - } - } - } - - private var heroSection: some View { - Section { - VStack(spacing: 16) { - TalkWaveformView( - phase: self.state.waveformPhase( - micLevel: self.appModel.talkMode.micLevel, - playbackLevel: self.appModel.talkMode.playbackLevel), - palette: .openClawBrand) - .frame(height: 130) - .accessibilityHidden(true) - - VStack(spacing: 4) { - Text(self.state.title) - .font(OpenClawType.title3SemiBold) - .multilineTextAlignment(.center) - self.heroSubtitle - .font(OpenClawType.subhead) - .foregroundStyle(.secondary) - .multilineTextAlignment(.center) - } - - Button(action: self.handlePrimaryAction) { - Label { - Text(self.state.primaryButtonTitle) - } icon: { - Image(systemName: self.state.primaryButtonIcon) - } - .font(OpenClawType.subheadSemiBold) - // Match the icon to the label; otherwise the symbol picks up the tint color. - .foregroundStyle(.white) - .frame(maxWidth: .infinity) - } - .buttonStyle(.borderedProminent) - .controlSize(.large) - .tint(self.state.color) - .disabled(self.state.primaryAction == .waiting) - } - .frame(maxWidth: .infinity) - .padding(.vertical, 8) - .listRowBackground(Color.clear) - } - } - - private var conversationSection: some View { - Section("Conversation") { - SettingsDetailRow("Agent", value: .verbatim(self.appModel.chatAgentName)) - SettingsDetailRow("Session", value: .verbatim(self.appModel.chatSessionKey)) - SettingsDetailRow("Runtime", value: .localized(self.appModel.talkMode.statusText)) - } - } - - private var voiceModeSection: some View { - Section("Voice Mode") { - SettingsDetailRow( - "Configured", - value: .localized(self.appModel.talkMode.gatewayTalkVoiceModeTitle)) - SettingsDetailRow("Active", value: .verbatim(self.activeModeText)) - SettingsDetailRow("Transport", value: .localized(self.transportText)) - if let issueText = self.talkIssueText { - SettingsDetailRow("Last issue", value: .verbatim(issueText)) - } - SettingsDetailRow("Permission", value: .localized(self.permissionText)) - SettingsDetailRow("Speech language", value: .verbatim(self.speechLocaleText)) - } - } - - private var controlsSection: some View { - Section("Controls") { - Toggle(isOn: self.talkSpeakerphoneBinding) { - Text("Speakerphone") - .font(OpenClawType.body) - } - .accessibilityIdentifier("talk-speakerphone-control") - Toggle(isOn: self.$talkBackgroundEnabled) { - Text("Background listening") - .font(OpenClawType.body) - } - .accessibilityIdentifier("talk-background-listening-control") - Button(action: self.openVoiceSettings) { - HStack { - Label("Voice & Talk Settings", systemImage: "slider.horizontal.3") - .font(OpenClawType.body) - .foregroundStyle(.primary) - Spacer() - Image(systemName: "chevron.forward") - .font(OpenClawType.footnoteSemiBold) - .foregroundStyle(.tertiary) - } - .contentShape(Rectangle()) - } - .buttonStyle(.plain) - .accessibilityIdentifier("talk-voice-settings-control") - } - } - - private var gatewayConnected: Bool { - !self.appModel.isAppleReviewDemoModeEnabled && - GatewayStatusBuilder.build(appModel: self.appModel) == .connected - } - - private var fallbackIssue: TalkRuntimeIssue? { - guard self.gatewayConnected else { return nil } - return self.appModel.talkMode.gatewayTalkCurrentFallbackIssue - } - - @ViewBuilder - private var heroSubtitle: some View { - if self.state.prefersPermissionCopy { - Text("Gateway approval is required before this phone can capture voice.") - .font(OpenClawType.subhead) - } else if self.appModel.isAppleReviewDemoModeEnabled { - Text("Voice is disabled in Apple Review demo mode.") - .font(OpenClawType.subhead) - } else if !self.gatewayConnected { - Text("Connect to your gateway to start a voice conversation.") - .font(OpenClawType.subhead) - } else if !self.appModel.talkMode.gatewayTalkConfigLoaded { - Text("Open Voice settings after the gateway loads Talk configuration.") - .font(OpenClawType.subhead) - } else { - let subtitle = (appModel.talkMode.gatewayTalkVoiceModeSubtitle ?? "") - .trimmingCharacters(in: .whitespacesAndNewlines) - if subtitle.isEmpty { - Text(verbatim: String( - format: String(localized: "Routes voice to %@."), - self.appModel.chatAgentName)) - .font(OpenClawType.subhead) - } else { - Text(verbatim: subtitle) - .font(OpenClawType.subhead) - } - } - } - - private var transportText: String { - let provider = self.appModel.talkMode.gatewayTalkProviderLabel.trimmingCharacters(in: .whitespacesAndNewlines) - let transport = self.appModel.talkMode.gatewayTalkTransportLabel.trimmingCharacters(in: .whitespacesAndNewlines) - if provider.isEmpty || provider == "Not loaded" { return transport.isEmpty ? "Not loaded" : transport } - if transport.isEmpty || transport == "Not loaded" { return provider } - return "\(provider) • \(transport)" - } - - private var activeModeText: String { - let title = self.appModel.talkMode.gatewayTalkActiveModeTitle.trimmingCharacters(in: .whitespacesAndNewlines) - let subtitle = (appModel.talkMode.gatewayTalkActiveModeSubtitle ?? "") - .trimmingCharacters(in: .whitespacesAndNewlines) - if title.isEmpty { return String(localized: "Not active") } - if subtitle.isEmpty { return title } - return "\(title) • \(subtitle)" - } - - private var talkIssueText: String? { - let text = (appModel.talkMode.gatewayTalkLastIssueText ?? "") - .trimmingCharacters(in: .whitespacesAndNewlines) - return text.isEmpty ? nil : text - } - - private var permissionText: String { - if let failure = appModel.talkMode.gatewayTalkPermissionState.failureMessage { - return failure - } - return self.appModel.talkMode.gatewayTalkPermissionState.statusLabel - } - - private var speechLocaleText: String { - if self.talkSpeechLocale == TalkSpeechLocale.automaticID { return "Automatic" } - return self.talkSpeechLocale - } - - private func alignPersistedTalkState() { - if self.appModel.isAppleReviewDemoModeEnabled, - self.talkEnabled || self.appModel.talkMode.isEnabled - { - self.stopTalk() - } else if self.appModel.talkMode.gatewayTalkPermissionState.requiresTalkPermissionAction, - self.talkEnabled || self.appModel.talkMode.isEnabled - { - self.stopTalk() - } else if self.talkEnabled != self.appModel.talkMode.isEnabled { - self.appModel.setTalkEnabled(self.talkEnabled) - } - } - - private var talkSpeakerphoneBinding: Binding { - Binding( - get: { self.talkSpeakerphoneEnabled }, - set: { enabled in - self.talkSpeakerphoneEnabled = enabled - self.appModel.setTalkSpeakerphoneEnabled(enabled) - }) - } - - private func handlePrimaryAction() { - switch self.state.primaryAction { - case .start: - self.startTalk() - case .stop: - self.stopTalk() - case .enablePermission: - self.stopTalk() - self.showPermissionPrompt = true - case .openSettings: - self.openPrimarySettings() - case .waiting: - break - } - } - - private func startTalk() { - guard !self.appModel.isAppleReviewDemoModeEnabled else { return } - self.talkEnabled = true - self.appModel.synchronizeTalkSessionKey() - self.appModel.setTalkEnabled(true) - } - - private func stopTalk() { - self.talkEnabled = false - self.appModel.setTalkEnabled(false) - } - - private func openPrimarySettings() { - if self.gatewayConnected { - self.openVoiceSettings() - } else { - self.openSettings() - } - } -} - -enum TalkProPrimaryAction: Equatable { - case start - case stop - case enablePermission - case openSettings - case waiting -} - -extension TalkWaveformPalette { - /// iOS app branding for the shared wave: adaptive accent front lobe plus - /// system grays so the idle wave tracks light/dark appearance. - static let openClawBrand = TalkWaveformPalette( - active: [ - OpenClawBrand.accent, - Color(red: 0.95, green: 0.45, blue: 0.30), - Color(red: 0.45, green: 0.08, blue: 0.12), - ], - inactive: [ - Color(uiColor: .systemGray2), - Color(uiColor: .systemGray3), - Color(uiColor: .systemGray4), - ]) -} - -struct TalkProState: Equatable { - let gatewayConnected: Bool - let isDemoMode: Bool - let isEnabled: Bool - let phase: TalkPhase - let isConfigLoaded: Bool - let isListening: Bool - let isSpeaking: Bool - let isUserSpeechDetected: Bool - let permissionState: TalkGatewayPermissionState - - var title: LocalizedStringResource { - if self.isDemoMode { return "Demo mode only" } - if !self.gatewayConnected { return "Gateway offline" } - switch self.permissionState { - case .missingScope, .requestFailed: - return "Gateway permission required" - case .requestingUpgrade: - return "Requesting approval" - case .upgradeRequested: - return "Approval requested" - case .apiKeyMissing: - return "Voice API key missing" - case .loadFailed: - return "Voice config failed" - default: - break - } - if !self.isConfigLoaded { return "Voice config unavailable" } - if self.isSpeaking { return "Speaking" } - if self.isListening { return "Listening" } - if self.phase == .connecting { return "Connecting" } - if self.phase == .thinking { return "Asking OpenClaw" } - if self.isEnabled { return "Ready to talk" } - return "Talk is off" - } - - var color: Color { - if self.isDemoMode { return .secondary } - if !self.gatewayConnected { return .secondary } - switch self.permissionState { - case .requestFailed, .loadFailed: - return OpenClawBrand.danger - case .missingScope, .requestingUpgrade, .upgradeRequested, .apiKeyMissing: - return OpenClawBrand.warn - default: - if !self.isConfigLoaded { return OpenClawBrand.warn } - return self.isEnabled ? OpenClawBrand.ok : OpenClawBrand.accentHot - } - } - - var primaryAction: TalkProPrimaryAction { - if self.isDemoMode { return .waiting } - if !self.gatewayConnected { return .openSettings } - switch self.permissionState { - case .missingScope, .requestFailed: - return .enablePermission - case .requestingUpgrade, .upgradeRequested: - return .waiting - case .apiKeyMissing, .loadFailed: - return .openSettings - default: - return self.isEnabled ? .stop : .start - } - } - - var primaryButtonTitle: LocalizedStringResource { - switch self.primaryAction { - case .start: "Start Talk" - case .stop: "Stop Talk" - case .enablePermission: "Enable Talk" - case .openSettings: self.gatewayConnected ? "Open Voice Settings" : "Open Gateway Settings" - case .waiting: self.isDemoMode ? "Demo Mode Only" : "Waiting for Approval" - } - } - - var primaryButtonIcon: String { - switch self.primaryAction { - case .start: "play.fill" - case .stop: "stop.fill" - case .enablePermission: "key.fill" - case .openSettings: "gearshape.fill" - case .waiting: self.isDemoMode ? "lock.fill" : "hourglass" - } - } - - var prefersPermissionCopy: Bool { - switch self.permissionState { - case .missingScope, .requestingUpgrade, .upgradeRequested, .requestFailed: - true - default: - false - } - } - - func waveformPhase(micLevel: Double, playbackLevel: Double?) -> TalkWaveformPhase { - if self.isDemoMode { return .idle } - if !self.gatewayConnected { return .idle } - switch self.permissionState { - case .requestingUpgrade, .upgradeRequested: - return .thinking - case .missingScope, .requestFailed, .apiKeyMissing, .loadFailed: - return .idle - default: - break - } - if !self.isConfigLoaded { return .idle } - if self.isSpeaking { return .speaking(level: playbackLevel) } - if self.isListening { return .listening(level: micLevel, speechActive: self.isUserSpeechDetected) } - if self.phase == .connecting || self.phase == .thinking { - return .thinking - } - return self.isEnabled ? .thinking : .idle - } -} diff --git a/apps/ios/Sources/LiveActivity/LiveActivityManager.swift b/apps/ios/Sources/LiveActivity/LiveActivityManager.swift index 8fc8b83052db..54a9872840c2 100644 --- a/apps/ios/Sources/LiveActivity/LiveActivityManager.swift +++ b/apps/ios/Sources/LiveActivity/LiveActivityManager.swift @@ -2,16 +2,48 @@ import Foundation import os -/// Minimal Live Activity lifecycle focused on connection health + stale cleanup. +/// Owns the single ActivityKit presentation for connection, attention, tool, +/// and voice state. Producers update independent inputs; the arbiter decides +/// which state is visible so lower-priority updates cannot hide urgent work. @MainActor final class LiveActivityManager { static let shared = LiveActivityManager() + private struct PendingActivityUpdate { + var state: OpenClawActivityAttributes.ContentState + var staleDate: Date? + } + + private struct StatusPresentation { + let status: OpenClawActivityAttributes.ContentState.Status + let verbatimDetail: String? + } + private let logger = Logger(subsystem: "ai.openclawfoundation.app", category: "LiveActivity") private let connectingStaleSeconds: TimeInterval = 120 + private let transientStaleSeconds: TimeInterval = 300 private let hydrationStaleSeconds: TimeInterval = 300 + private let voiceSamplePublishDelay = Duration.milliseconds(500) + private let voiceStaleRefreshDelay = Duration.seconds(240) + private let toolStaleRefreshDelay = Duration.seconds(240) + + private var arbiter = LiveActivityPresentationArbiter() private var currentActivity: Activity? - private var activityStartDate: Date = .now + private var currentState: OpenClawActivityAttributes.ContentState? + private var currentStaleDate: Date? + private var pendingActivityUpdate: PendingActivityUpdate? + private var activityUpdateTask: Task? + private var activityGeneration: UInt64 = 0 + private var voiceSampleBuffer = LiveActivityVoiceSampleBuffer() + private var pendingVoiceSample: UInt8? + private var voiceSampleTask: Task? + private var voiceStaleRefreshTask: Task? + private var voiceStaleRefreshGeneration: UInt64 = 0 + private var toolStaleRefreshTask: Task? + private var toolStaleRefreshGeneration: UInt64 = 0 + #if DEBUG + private var voicePreviewActive = false + #endif private init() { self.hydrateCurrentAndPruneDuplicates() @@ -23,146 +55,510 @@ final class LiveActivityManager { sessionKey: String) { let presentation = Self.connectingPresentation(statusText: statusText) - self.hydrateCurrentAndPruneDuplicates() + let startedAt = Self.lifecycleStartedAt( + existing: self.arbiter.connection, + agentName: agentName, + sessionKey: sessionKey) + let state = OpenClawActivityAttributes.ContentState( + status: presentation.status, + verbatimDetail: presentation.verbatimDetail, + startedAt: startedAt) + self.arbiter.setConnection(self.request( + state: state, + staleAfter: self.connectingStaleSeconds, + agentName: agentName, + sessionKey: sessionKey)) + self.reconcile(reason: "connecting") + } - if self.currentActivity != nil { - self.handleConnecting(presentation: presentation) + func showAttention(statusText: String, agentName: String, sessionKey: String) { + let presentation = Self.attentionPresentation(statusText: statusText) + let startedAt = Self.lifecycleStartedAt( + existing: self.arbiter.attention, + agentName: agentName, + sessionKey: sessionKey) + let state = OpenClawActivityAttributes.ContentState( + status: presentation.status, + verbatimDetail: presentation.verbatimDetail, + startedAt: startedAt) + self.arbiter.setAttention(self.request( + state: state, + staleAfter: nil, + agentName: agentName, + sessionKey: sessionKey)) + self.reconcile(reason: "attention") + } + + func showTool(id: String, name: String, agentName: String, agentBadge: String, sessionKey: String) { + let toolName = name.trimmingCharacters(in: .whitespacesAndNewlines) + guard !id.isEmpty, !toolName.isEmpty else { return } + let state = OpenClawActivityAttributes.ContentState( + status: .toolRunning, + verbatimDetail: nil, + startedAt: .now, + agentBadge: agentBadge, + toolName: toolName) + self.arbiter.startTool( + id: id, + request: self.request( + state: state, + staleAfter: self.transientStaleSeconds, + agentName: agentName, + sessionKey: sessionKey)) + self.ensureToolStaleRefreshTask() + self.reconcile(reason: "tool_started") + } + + func endTool(id: String, sessionKey: String) { + self.arbiter.endTool(id: id, sessionKey: sessionKey) + if self.arbiter.activeToolCount == 0 { + self.stopToolStaleRefreshTask() + } + self.reconcile(reason: "tool_finished") + } + + func showVoice( + statusText: String, + isListening: Bool, + isSpeaking: Bool, + audioLevel: Double?, + agentName: String, + agentBadge: String, + sessionKey: String) + { + let status = LiveActivityPresentationArbiter.voiceStatus( + isListening: isListening, + isSpeaking: isSpeaking) + let detail = Self.voiceDetail(statusText, status: status) + let previousVoice = self.arbiter.voice + let hasSameOwner = Self.hasSameOwner( + previousVoice, + agentName: agentName, + sessionKey: sessionKey) + let startedAt = Self.lifecycleStartedAt( + existing: previousVoice, + agentName: agentName, + sessionKey: sessionKey) + let hasMeasuredAudio = isSpeaking || isListening + if !hasSameOwner || Self.shouldResetVoiceSamples(previousStatus: previousVoice?.state.status) { + self.resetVoiceSamples() + } + if hasMeasuredAudio, let sample = LiveActivityVoiceSampleBuffer.quantize(audioLevel) { + self.voiceSampleBuffer.append(sample) + } + let state = OpenClawActivityAttributes.ContentState( + status: status, + verbatimDetail: detail, + startedAt: startedAt, + agentBadge: agentBadge, + voiceSamples: hasMeasuredAudio ? self.voiceSampleBuffer.payload : nil) + self.arbiter.setVoice(self.request( + state: state, + staleAfter: self.transientStaleSeconds, + agentName: agentName, + sessionKey: sessionKey)) + self.ensureVoiceStaleRefreshTask() + self.reconcile(reason: "voice_changed") + } + + /// Coalesces the high-rate playback meter into a bounded two-updates-per-second + /// envelope. The widget gets real motion without building an ActivityKit queue. + func updateVoiceLevel(_ level: Double?) { + guard let status = arbiter.voice?.state.status, + status == .voiceSpeaking || status == .voiceListening, + let sample = LiveActivityVoiceSampleBuffer.quantize(level) + else { return } + self.pendingVoiceSample = sample + guard self.voiceSampleTask == nil else { return } + self.voiceSampleTask = Task { [weak self] in + guard let self else { return } + try? await Task.sleep(for: self.voiceSamplePublishDelay) + guard !Task.isCancelled else { return } + self.voiceSampleTask = nil + self.publishPendingVoiceSample() + } + } + + func endVoice() { + #if DEBUG + guard !self.voicePreviewActive else { return } + #endif + self.resetVoiceSamples() + self.stopVoiceStaleRefreshTask() + self.arbiter.setVoice(nil) + self.reconcile(reason: "voice_finished") + } + + #if DEBUG + /// Simulator-only fixture. The widget supplies its own bounded animation; + /// ActivityKit receives a state transition rather than audio-rate updates. + func startVoicePreview() { + self.voicePreviewActive = true + self.showVoice( + statusText: "Speaking", + isListening: false, + isSpeaking: true, + audioLevel: nil, + agentName: "Aiden", + agentBadge: "🐕", + sessionKey: "live-activity-preview") + } + #endif + + func handleReconnect() { + self.arbiter.clearConnectionState() + self.reconcile(reason: "connected") + } + + func endActivity(reason: String) { + self.arbiter.clearAll() + self.stopToolStaleRefreshTask() + self.stopVoiceStaleRefreshTask() + self.resetVoiceSamples() + #if DEBUG + self.voicePreviewActive = false + #endif + self.endCurrentActivity(reason: reason) + } + + private func request( + state: OpenClawActivityAttributes.ContentState, + staleAfter seconds: TimeInterval?, + agentName: String, + sessionKey: String) -> LiveActivityPresentationRequest + { + LiveActivityPresentationRequest( + state: state, + staleDate: seconds.map { Date().addingTimeInterval($0) }, + agentName: agentName, + sessionKey: sessionKey) + } + + private func reconcile(reason: String) { + guard let request = arbiter.current else { + self.endCurrentActivity(reason: reason) return } - let authInfo = ActivityAuthorizationInfo() - guard authInfo.areActivitiesEnabled else { + if let activity = currentActivity, + activity.activityState == .active, + activity.attributes.agentName != request.agentName || + activity.attributes.sessionKey != request.sessionKey + { + self.endCurrentActivity(reason: "context_changed") + self.startActivity(request) + return + } + + if let activity = currentActivity, activity.activityState == .active { + self.enqueueLatestUpdate( + activity: activity, + state: request.state, + staleDate: request.staleDate) + return + } + + if let activity = currentActivity { + // An inactive activity can still have a suspended update worker. Retire + // that generation before replacing it or the new activity never starts one. + self.activityGeneration &+= 1 + self.activityUpdateTask?.cancel() + self.activityUpdateTask = nil + self.pendingActivityUpdate = nil + self.end(activity: activity) + self.currentActivity = nil + self.currentState = nil + self.currentStaleDate = nil + } + self.startActivity(request) + } + + private func publishPendingVoiceSample() { + guard let sample = pendingVoiceSample, + var request = arbiter.voice, + request.state.status == .voiceSpeaking || request.state.status == .voiceListening + else { + self.pendingVoiceSample = nil + return + } + self.pendingVoiceSample = nil + self.voiceSampleBuffer.append(sample) + request.state.voiceSamples = self.voiceSampleBuffer.payload + request.staleDate = Date().addingTimeInterval(self.transientStaleSeconds) + self.arbiter.setVoice(request) + self.reconcile(reason: "voice_level") + } + + private func resetVoiceSamples() { + self.voiceSampleTask?.cancel() + self.voiceSampleTask = nil + self.pendingVoiceSample = nil + self.voiceSampleBuffer.reset() + } + + /// Active tool calls refresh their bounded deadline while this process owns + /// them. Suspension or termination stops the heartbeat so stale work pauses. + private func ensureToolStaleRefreshTask() { + guard self.toolStaleRefreshTask == nil, self.arbiter.activeToolCount > 0 else { return } + self.toolStaleRefreshGeneration &+= 1 + let generation = self.toolStaleRefreshGeneration + self.toolStaleRefreshTask = Task { [weak self] in + while !Task.isCancelled { + guard let self else { return } + try? await Task.sleep(for: self.toolStaleRefreshDelay) + guard !Task.isCancelled, + generation == self.toolStaleRefreshGeneration, + self.arbiter.activeToolCount > 0 + else { break } + self.arbiter.refreshTools( + staleDate: Date().addingTimeInterval(self.transientStaleSeconds)) + self.reconcile(reason: "tool_heartbeat") + } + guard let self, generation == self.toolStaleRefreshGeneration else { return } + self.toolStaleRefreshTask = nil + } + } + + private func stopToolStaleRefreshTask() { + self.toolStaleRefreshGeneration &+= 1 + self.toolStaleRefreshTask?.cancel() + self.toolStaleRefreshTask = nil + } + + private func ensureVoiceStaleRefreshTask() { + guard self.voiceStaleRefreshTask == nil, self.arbiter.voice != nil else { return } + self.voiceStaleRefreshGeneration &+= 1 + let generation = self.voiceStaleRefreshGeneration + self.voiceStaleRefreshTask = Task { [weak self] in + while !Task.isCancelled { + guard let self else { return } + try? await Task.sleep(for: self.voiceStaleRefreshDelay) + guard !Task.isCancelled, + generation == self.voiceStaleRefreshGeneration, + self.arbiter.voice != nil + else { break } + self.arbiter.refreshVoice( + staleDate: Date().addingTimeInterval(self.transientStaleSeconds)) + self.reconcile(reason: "voice_heartbeat") + } + guard let self, generation == self.voiceStaleRefreshGeneration else { return } + self.voiceStaleRefreshTask = nil + } + } + + private func stopVoiceStaleRefreshTask() { + self.voiceStaleRefreshGeneration &+= 1 + self.voiceStaleRefreshTask?.cancel() + self.voiceStaleRefreshTask = nil + } + + private func startActivity(_ request: LiveActivityPresentationRequest) { + guard ActivityAuthorizationInfo().areActivitiesEnabled else { self.logger.info("Live Activities disabled; skipping start") return } - self.activityStartDate = .now - let attributes = OpenClawActivityAttributes(agentName: agentName, sessionKey: sessionKey) - let state = self.connectingState(presentation: presentation) - do { let activity = try Activity.request( - attributes: attributes, - content: ActivityContent( - state: state, - staleDate: Date().addingTimeInterval(self.connectingStaleSeconds)), + attributes: OpenClawActivityAttributes( + agentName: request.agentName, + sessionKey: request.sessionKey), + content: ActivityContent(state: request.state, staleDate: request.staleDate), pushType: nil) + self.activityGeneration &+= 1 self.currentActivity = activity + self.currentState = request.state + self.currentStaleDate = request.staleDate self.logger.info("started live activity id=\(activity.id, privacy: .public)") } catch { self.logger.error("failed to start live activity: \(error.localizedDescription, privacy: .public)") } } - func showAttention(statusText: String, agentName: String, sessionKey: String) { - let presentation = Self.attentionPresentation(statusText: statusText) - self.hydrateCurrentAndPruneDuplicates() + /// Keeps at most one ActivityKit update in flight and one replaceable next + /// value. Slow or throttled ActivityKit writes cannot build an obsolete queue. + private func enqueueLatestUpdate( + activity: Activity, + state: OpenClawActivityAttributes.ContentState, + staleDate: Date?) + { + guard state != self.currentState || staleDate != self.currentStaleDate else { return } + self.currentState = state + self.currentStaleDate = staleDate + self.pendingActivityUpdate = PendingActivityUpdate(state: state, staleDate: staleDate) + guard self.activityUpdateTask == nil else { return } + self.startUpdateWorker(activity: activity, generation: self.activityGeneration) + } - if self.currentActivity == nil { - let authInfo = ActivityAuthorizationInfo() - guard authInfo.areActivitiesEnabled else { - self.logger.info("Live Activities disabled; skipping attention state") - return + private func startUpdateWorker(activity: Activity, generation: UInt64) { + self.activityUpdateTask = Task { [weak self] in + while !Task.isCancelled { + guard let self, + generation == self.activityGeneration, + self.currentActivity?.id == activity.id, + let pending = self.pendingActivityUpdate + else { break } + self.pendingActivityUpdate = nil + await activity.update(ActivityContent(state: pending.state, staleDate: pending.staleDate)) } - self.activityStartDate = .now - let attributes = OpenClawActivityAttributes(agentName: agentName, sessionKey: sessionKey) - do { - let activity = try Activity.request( - attributes: attributes, - content: ActivityContent(state: self.attentionState(presentation: presentation), staleDate: nil), - pushType: nil) - self.currentActivity = activity - self.logger.info("started attention live activity id=\(activity.id, privacy: .public)") - } catch { - self.logger.error( - "failed to start attention live activity: \(error.localizedDescription, privacy: .public)") + + guard let self, generation == self.activityGeneration else { return } + self.activityUpdateTask = nil + if self.pendingActivityUpdate != nil, + self.currentActivity?.id == activity.id + { + self.startUpdateWorker(activity: activity, generation: generation) } - return } - - self.updateCurrent(state: self.attentionState(presentation: presentation), staleDate: nil) } - private func handleConnecting(presentation: StatusPresentation) { - self.updateCurrent( - state: self.connectingState(presentation: presentation), - staleDate: Date().addingTimeInterval(self.connectingStaleSeconds)) - } - - func handleReconnect() { - self.endActivity(reason: "connected") - } - - func endActivity(reason: String) { - guard let activity = self.currentActivity else { return } + private func endCurrentActivity(reason: String) { + guard let activity = currentActivity else { return } + let startedAt = self.currentState?.startedAt ?? .now + self.activityGeneration &+= 1 + self.activityUpdateTask?.cancel() + self.activityUpdateTask = nil + self.pendingActivityUpdate = nil self.currentActivity = nil + self.currentState = nil + self.currentStaleDate = nil self.logger.info("ending live activity reason=\(reason, privacy: .public)") + let finalState = OpenClawActivityAttributes.ContentState( + status: .disconnected, + verbatimDetail: nil, + startedAt: startedAt) Task { await activity.end( - ActivityContent(state: self.disconnectedState(), staleDate: nil), + ActivityContent(state: finalState, staleDate: nil), dismissalPolicy: .immediate) } } private func hydrateCurrentAndPruneDuplicates() { let active = Activity.activities - guard !active.isEmpty else { - self.currentActivity = nil - return - } + guard !active.isEmpty else { return } let now = Date() let candidates = active.filter { activity in let state = activity.content.state guard activity.activityState == .active else { return false } - guard state.status != .idle, state.status != .disconnected else { return false } - return now.timeIntervalSince(state.startedAt) < self.hydrationStaleSeconds + return Self.shouldHydrate( + status: state.status, + startedAt: state.startedAt, + staleDate: activity.content.staleDate, + now: now, + maximumStartAge: self.hydrationStaleSeconds) } - guard !candidates.isEmpty else { - self.currentActivity = nil + guard let keeper = candidates.max(by: { + $0.content.state.startedAt < $1.content.state.startedAt + }) else { for activity in active { self.end(activity: activity) } return } - let keeper = candidates.max { lhs, rhs in - lhs.content.state.startedAt < rhs.content.state.startedAt - } ?? candidates[0] - + self.activityGeneration &+= 1 self.currentActivity = keeper - self.activityStartDate = keeper.content.state.startedAt - - let stale = active.filter { $0.id != keeper.id } - for activity in stale { + self.currentState = keeper.content.state + self.currentStaleDate = keeper.content.staleDate + var request = LiveActivityPresentationRequest( + state: keeper.content.state, + staleDate: keeper.content.staleDate, + agentName: keeper.attributes.agentName, + sessionKey: keeper.attributes.sessionKey) + switch keeper.content.state.status { + case .approvalNeeded, .actionRequired, .attention: + self.arbiter.setAttention(request) + case .voiceActive, .voiceListening, .voiceSpeaking: + self.arbiter.setVoice(request) + case .toolRunning: + // A hydrated tool has no invocation ID to balance. Keep it as the + // lowest-priority fallback until a live producer replaces it. + request.staleDate = Date().addingTimeInterval(self.transientStaleSeconds) + self.arbiter.adoptInitialHydratedToolFallback(request) + case .connecting, .reconnecting, .paused: + self.arbiter.setConnection(request) + case .idle, .disconnected: + break + } + for activity in active where activity.id != keeper.id { self.end(activity: activity) } + // Hydrated tool fallbacks renew their bounded lifetime above. Reconcile + // that request so ActivityKit receives the renewed stale date too. + self.reconcile(reason: "hydrate") + } + + /// Voice and tool activities renew `staleDate` while their producer is + /// alive. Their original start time can therefore be old without making a + /// heartbeat-refreshed presentation obsolete after process relaunch. + nonisolated static func shouldHydrate( + status: OpenClawActivityAttributes.ContentState.Status, + startedAt: Date, + staleDate: Date?, + now: Date, + maximumStartAge: TimeInterval = 300) -> Bool + { + guard status != .idle, status != .disconnected else { return false } + if let staleDate, staleDate <= now { + return false + } + switch status { + case .approvalNeeded, .actionRequired, .attention: + return true + case .voiceActive, .voiceListening, .voiceSpeaking, .toolRunning: + return staleDate != nil || now.timeIntervalSince(startedAt) < maximumStartAge + case .connecting, .reconnecting, .paused: + return now.timeIntervalSince(startedAt) < maximumStartAge + case .idle, .disconnected: + return false + } } - private func updateCurrent(state: OpenClawActivityAttributes.ContentState, staleDate: Date? = nil) { - guard let activity = self.currentActivity, activity.activityState == .active else { - self.currentActivity = nil - return - } - Task { - await activity.update(ActivityContent(state: state, staleDate: staleDate)) + /// A live voice producer owns the waveform buffer. Phase changes preserve + /// its recent envelope; only a newly adopted producer starts a new trace. + nonisolated static func shouldResetVoiceSamples( + previousStatus: OpenClawActivityAttributes.ContentState.Status?) -> Bool + { + previousStatus == nil + } + + nonisolated static func hasSameOwner( + _ existing: LiveActivityPresentationRequest?, + agentName: String, + sessionKey: String) -> Bool + { + existing?.agentName == agentName && existing?.sessionKey == sessionKey + } + + nonisolated static func lifecycleStartedAt( + existing: LiveActivityPresentationRequest?, + agentName: String, + sessionKey: String, + now: Date = .now) -> Date + { + guard self.hasSameOwner(existing, agentName: agentName, sessionKey: sessionKey) else { + return now } + return existing?.state.startedAt ?? now } private func end(activity: Activity) { + let startedAt = activity.content.state.startedAt Task { await activity.end( - ActivityContent(state: self.disconnectedState(), staleDate: nil), + ActivityContent( + state: OpenClawActivityAttributes.ContentState( + status: .disconnected, + verbatimDetail: nil, + startedAt: startedAt), + staleDate: nil), dismissalPolicy: .immediate) } } - private struct StatusPresentation { - let status: OpenClawActivityAttributes.ContentState.Status - let verbatimDetail: String? - } - /// Existing callers still pass rendered app copy. Collapse known values here so /// ActivityKit persists semantics; only unknown external detail remains verbatim. private static func connectingPresentation(statusText: String) -> StatusPresentation { @@ -190,24 +586,31 @@ final class LiveActivityManager { return trimmed.isEmpty ? nil : trimmed } - private func connectingState(presentation: StatusPresentation) -> OpenClawActivityAttributes.ContentState { - OpenClawActivityAttributes.ContentState( - status: presentation.status, - verbatimDetail: presentation.verbatimDetail, - startedAt: self.activityStartDate) - } - - private func attentionState(presentation: StatusPresentation) -> OpenClawActivityAttributes.ContentState { - OpenClawActivityAttributes.ContentState( - status: presentation.status, - verbatimDetail: presentation.verbatimDetail, - startedAt: self.activityStartDate) - } - - private func disconnectedState() -> OpenClawActivityAttributes.ContentState { - OpenClawActivityAttributes.ContentState( - status: .disconnected, - verbatimDetail: nil, - startedAt: self.activityStartDate) + private static func voiceDetail( + _ value: String, + status: OpenClawActivityAttributes.ContentState.Status) -> String? + { + let knownLabels: [String] = switch status { + case .voiceListening: + [ + String(localized: "Listening"), + String(localized: "Listening (PTT)"), + String(localized: "Listening (Realtime)"), + ] + case .voiceSpeaking: + [ + String(localized: "Speaking…"), + String(localized: "Speaking (System)…"), + String(localized: "Generating voice…"), + ] + case .voiceActive: + [String(localized: "Ready")] + default: + [] + } + if knownLabels.contains(value) { + return nil + } + return self.normalizedDetail(value) } } diff --git a/apps/ios/Sources/LiveActivity/LiveActivityPresentationArbiter.swift b/apps/ios/Sources/LiveActivity/LiveActivityPresentationArbiter.swift new file mode 100644 index 000000000000..ea6b0b4190f0 --- /dev/null +++ b/apps/ios/Sources/LiveActivity/LiveActivityPresentationArbiter.swift @@ -0,0 +1,150 @@ +import Foundation + +struct LiveActivityPresentationRequest: Equatable { + var state: OpenClawActivityAttributes.ContentState + var staleDate: Date? + var agentName: String + var sessionKey: String +} + +struct LiveActivityVoiceSampleBuffer { + private(set) var values: [UInt8] = [] + let capacity: Int + + init(capacity: Int = 24) { + self.capacity = max(capacity, 1) + } + + var payload: [UInt8]? { + self.values.isEmpty ? nil : self.values + } + + static func quantize(_ level: Double?) -> UInt8? { + guard let level, level.isFinite else { return nil } + let clamped = min(max(level, 0), 1) + return UInt8((clamped * 255).rounded()) + } + + mutating func append(_ sample: UInt8) { + self.values.append(sample) + if self.values.count > self.capacity { + self.values.removeFirst(self.values.count - self.capacity) + } + } + + mutating func reset() { + self.values.removeAll(keepingCapacity: true) + } +} + +/// Keeps independent producers from overwriting higher-priority Live Activity +/// state. ActivityKit receives one reconciled presentation instead of competing +/// connection, tool, and voice writes. +struct LiveActivityPresentationArbiter { + private struct ToolIdentity: Hashable { + let sessionKey: String + let id: String + } + + private(set) var connection: LiveActivityPresentationRequest? + private(set) var attention: LiveActivityPresentationRequest? + private(set) var voice: LiveActivityPresentationRequest? + private(set) var hydratedToolFallback: LiveActivityPresentationRequest? + private var toolsByIdentity: [ToolIdentity: LiveActivityPresentationRequest] = [:] + private var toolOrder: [ToolIdentity] = [] + + var current: LiveActivityPresentationRequest? { + if let attention { + return attention + } + if let toolIdentity = toolOrder.last, + let tool = toolsByIdentity[toolIdentity] + { + return tool + } + return self.voice ?? self.connection ?? self.hydratedToolFallback + } + + var activeToolCount: Int { + self.toolsByIdentity.count + } + + static func voiceStatus( + isListening: Bool, + isSpeaking: Bool) -> OpenClawActivityAttributes.ContentState.Status + { + if isSpeaking { + return .voiceSpeaking + } + if isListening { + return .voiceListening + } + return .voiceActive + } + + mutating func setConnection(_ request: LiveActivityPresentationRequest?) { + if request != nil { + self.hydratedToolFallback = nil + } + self.connection = request + } + + mutating func setAttention(_ request: LiveActivityPresentationRequest?) { + self.attention = request + } + + mutating func setVoice(_ request: LiveActivityPresentationRequest?) { + if request != nil { + self.hydratedToolFallback = nil + } + self.voice = request + } + + /// Called only by synchronous process-start hydration before the manager + /// escapes its initializer. Live producers consume this one-time fallback. + mutating func adoptInitialHydratedToolFallback(_ request: LiveActivityPresentationRequest?) { + self.hydratedToolFallback = request + } + + mutating func refreshVoice(staleDate: Date) { + self.voice?.staleDate = staleDate + } + + mutating func startTool(id: String, request: LiveActivityPresentationRequest) { + guard !id.isEmpty else { return } + self.hydratedToolFallback = nil + let identity = ToolIdentity(sessionKey: request.sessionKey, id: id) + if self.toolsByIdentity[identity] == nil { + self.toolOrder.append(identity) + } + self.toolsByIdentity[identity] = request + } + + mutating func endTool(id: String, sessionKey: String) { + self.hydratedToolFallback = nil + let identity = ToolIdentity(sessionKey: sessionKey, id: id) + self.toolsByIdentity[identity] = nil + self.toolOrder.removeAll { $0 == identity } + } + + mutating func refreshTools(staleDate: Date) { + for identity in self.toolsByIdentity.keys { + self.toolsByIdentity[identity]?.staleDate = staleDate + } + } + + mutating func clearConnectionState() { + self.connection = nil + self.attention = nil + self.hydratedToolFallback = nil + } + + mutating func clearAll() { + self.connection = nil + self.attention = nil + self.voice = nil + self.hydratedToolFallback = nil + self.toolsByIdentity.removeAll(keepingCapacity: true) + self.toolOrder.removeAll(keepingCapacity: true) + } +} diff --git a/apps/ios/Sources/LiveActivity/OpenClawActivityAttributes.swift b/apps/ios/Sources/LiveActivity/OpenClawActivityAttributes.swift index 7bdc7160ca52..f021d403277c 100644 --- a/apps/ios/Sources/LiveActivity/OpenClawActivityAttributes.swift +++ b/apps/ios/Sources/LiveActivity/OpenClawActivityAttributes.swift @@ -13,6 +13,11 @@ struct OpenClawActivityAttributes: ActivityAttributes { case approvalNeeded case actionRequired case attention + case toolRunning + case voiceListening + case voiceSpeaking + case voiceActive + case paused case idle case disconnected } @@ -20,11 +25,19 @@ struct OpenClawActivityAttributes: ActivityAttributes { var status: Status var verbatimDetail: String? var startedAt: Date + var agentBadge: String? + var toolName: String? + /// Recent playback-envelope samples, oldest first, quantized from 0...1. + /// Live Activity updates carry the real audible signal across the app/widget boundary. + var voiceSamples: [UInt8]? private enum CodingKeys: String, CodingKey { case status case verbatimDetail case startedAt + case agentBadge + case toolName + case voiceSamples } private enum LegacyCodingKeys: String, CodingKey { @@ -34,10 +47,20 @@ struct OpenClawActivityAttributes: ActivityAttributes { case isConnecting } - init(status: Status, verbatimDetail: String?, startedAt: Date) { + init( + status: Status, + verbatimDetail: String?, + startedAt: Date, + agentBadge: String? = nil, + toolName: String? = nil, + voiceSamples: [UInt8]? = nil) + { self.status = status self.verbatimDetail = verbatimDetail self.startedAt = startedAt + self.agentBadge = agentBadge + self.toolName = toolName + self.voiceSamples = voiceSamples } init(from decoder: Decoder) throws { @@ -47,6 +70,9 @@ struct OpenClawActivityAttributes: ActivityAttributes { if let status = try container.decodeIfPresent(Status.self, forKey: .status) { self.status = status self.verbatimDetail = try container.decodeIfPresent(String.self, forKey: .verbatimDetail) + self.agentBadge = try container.decodeIfPresent(String.self, forKey: .agentBadge) + self.toolName = try container.decodeIfPresent(String.self, forKey: .toolName) + self.voiceSamples = try container.decodeIfPresent([UInt8].self, forKey: .voiceSamples) return } @@ -59,8 +85,11 @@ struct OpenClawActivityAttributes: ActivityAttributes { isIdle: legacy.decodeIfPresent(Bool.self, forKey: .isIdle) ?? false, isDisconnected: legacy.decodeIfPresent(Bool.self, forKey: .isDisconnected) ?? false, isConnecting: legacy.decodeIfPresent(Bool.self, forKey: .isConnecting) ?? false) - self.status = presentation.status + status = presentation.status self.verbatimDetail = presentation.verbatimDetail + self.agentBadge = nil + self.toolName = nil + self.voiceSamples = nil } func encode(to encoder: Encoder) throws { @@ -68,6 +97,9 @@ struct OpenClawActivityAttributes: ActivityAttributes { try container.encode(self.status, forKey: .status) try container.encodeIfPresent(self.verbatimDetail, forKey: .verbatimDetail) try container.encode(self.startedAt, forKey: .startedAt) + try container.encodeIfPresent(self.agentBadge, forKey: .agentBadge) + try container.encodeIfPresent(self.toolName, forKey: .toolName) + try container.encodeIfPresent(self.voiceSamples, forKey: .voiceSamples) } private static func legacyPresentation( diff --git a/apps/ios/Sources/LiveActivity/VoiceLiveActivityCoordinator.swift b/apps/ios/Sources/LiveActivity/VoiceLiveActivityCoordinator.swift new file mode 100644 index 000000000000..f39e47b72296 --- /dev/null +++ b/apps/ios/Sources/LiveActivity/VoiceLiveActivityCoordinator.swift @@ -0,0 +1,85 @@ +import Observation + +/// Keeps Talk's ActivityKit presentation alive independently of whichever app +/// surface is mounted. Chat, Control, and iPad can all start Talk, so no view +/// may own this lifecycle. +@MainActor +final class VoiceLiveActivityCoordinator { + private weak var appModel: NodeAppModel? + + func start(appModel: NodeAppModel) { + guard self.appModel == nil else { return } + self.appModel = appModel + self.syncPresentation() + self.observePresentationState() + self.observeAudioLevel() + } + + private func observePresentationState() { + guard let appModel else { return } + withObservationTracking { + _ = appModel.talkMode.isEnabled + _ = appModel.talkMode.statusText + _ = appModel.talkMode.isSpeaking + _ = appModel.talkMode.isListening + _ = appModel.chatSessionKey + _ = appModel.chatAgentName + _ = appModel.chatAgentAvatarText + } onChange: { [weak self] in + Task { @MainActor in + guard let self else { return } + self.syncPresentation() + self.observePresentationState() + } + } + } + + private func observeAudioLevel() { + guard let appModel else { return } + withObservationTracking { + _ = appModel.talkMode.isSpeaking + _ = appModel.talkMode.isListening + _ = appModel.talkMode.playbackLevel + _ = appModel.talkMode.micLevel + } onChange: { [weak self] in + Task { @MainActor in + guard let self else { return } + self.syncAudioLevel() + self.observeAudioLevel() + } + } + } + + private func syncPresentation() { + guard let appModel else { return } + guard appModel.talkMode.isEnabled else { + LiveActivityManager.shared.endVoice() + return + } + LiveActivityManager.shared.showVoice( + statusText: appModel.talkMode.statusText, + isListening: appModel.talkMode.isListening, + isSpeaking: appModel.talkMode.isSpeaking, + audioLevel: Self.currentAudioLevel(appModel: appModel), + agentName: appModel.chatAgentName, + agentBadge: AgentIdentityPresentation.badge( + avatarText: appModel.chatAgentAvatarText, + displayName: appModel.chatAgentName), + sessionKey: appModel.chatSessionKey) + } + + private func syncAudioLevel() { + guard let appModel else { return } + LiveActivityManager.shared.updateVoiceLevel(Self.currentAudioLevel(appModel: appModel)) + } + + private static func currentAudioLevel(appModel: NodeAppModel) -> Double? { + if appModel.talkMode.isSpeaking { + return appModel.talkMode.playbackLevel + } + if appModel.talkMode.isListening { + return appModel.talkMode.micLevel + } + return nil + } +} diff --git a/apps/ios/Sources/Model/AgentIdentityPresentation.swift b/apps/ios/Sources/Model/AgentIdentityPresentation.swift new file mode 100644 index 000000000000..3f834088c3fa --- /dev/null +++ b/apps/ios/Sources/Model/AgentIdentityPresentation.swift @@ -0,0 +1,21 @@ +import Foundation + +enum AgentIdentityPresentation { + nonisolated static func badge(avatarText: String?, displayName: String) -> String { + self.normalizedBadgeEmoji(avatarText) ?? self.initialsBadge(for: displayName) + } + + nonisolated static func initialsBadge(for displayName: String) -> String { + let words = displayName + .split(whereSeparator: { $0.isWhitespace || $0 == "-" || $0 == "_" }) + .prefix(2) + let initials = words.compactMap(\.first).map(String.init).joined() + return initials.isEmpty ? "OC" : initials.uppercased() + } + + nonisolated static func normalizedBadgeEmoji(_ value: String?) -> String? { + guard let value else { return nil } + let normalized = value.trimmingCharacters(in: .whitespacesAndNewlines) + return normalized.isEmpty || normalized == "?" ? nil : normalized + } +} diff --git a/apps/ios/Sources/Model/NodeAppModel.swift b/apps/ios/Sources/Model/NodeAppModel.swift index c72af0ec18c8..ab1ab35c61fd 100644 --- a/apps/ios/Sources/Model/NodeAppModel.swift +++ b/apps/ios/Sources/Model/NodeAppModel.swift @@ -88,6 +88,11 @@ private enum IOSDeepLinkAgentPolicy { static let maxUnkeyedConfirmChars = 240 } +private enum TalkCapturePreparationOwner { + case remotePushToTalk(epoch: UInt64) + case chatDictation(epoch: UInt64) +} + @MainActor @Observable // swiftlint:disable type_body_length file_length @@ -514,9 +519,13 @@ final class NodeAppModel { @ObservationIgnored private var gatewaySessionResetGeneration: UInt64 = 0 @ObservationIgnored private var gatewayRouteGeneration: UInt64 = 0 @ObservationIgnored private var operatorTalkConnectionGeneration: UInt64 = 0 + @ObservationIgnored private var operatorTalkHydrationGeneration: UInt64? @ObservationIgnored private var credentialHandoffFailureGeneration: UInt64? @ObservationIgnored private(set) var gatewayConnectGeneration: UInt64 = 0 private var forceOperatorTalkPermissionUpgradeRequest = false + @ObservationIgnored private var talkPermissionUpgradeTask: Task? + @ObservationIgnored private var talkPermissionUpgradeReconnectTask: Task? + private var talkPermissionUpgradeReconnectGeneration: UInt64 = 0 private var lastTalkPermissionReconnectAttemptAt: Date? private var voiceWakeSyncTask: Task? @ObservationIgnored private var cameraHUDDismissTask: Task? @@ -549,7 +558,9 @@ final class NodeAppModel { @ObservationIgnored private var testExecApprovalResolutionReconcilesUnknownAck = false #endif private var pttVoiceWakeLeaseCaptureId: String? + private var chatDictationCaptureId: String? private var talkPttCommandEpoch: UInt64 = 0 + private var chatDictationCommandEpoch: UInt64 = 0 private var talkPreparationInFlight = false private var auxiliaryAudioCapture: AuxiliaryAudioCapture? private var foregroundCaptureCancellations: [UUID: @MainActor () -> Void] = [:] @@ -612,6 +623,10 @@ final class NodeAppModel { self.pttVoiceWakeLeaseCaptureId != nil } + var isChatDictationActive: Bool { + self.chatDictationCaptureId != nil + } + var localChatFixture: LocalChatFixture? { if self.isScreenshotFixtureModeEnabled { return .appScreenshots } if self.isAppleReviewDemoModeEnabled { return .appleReviewDemo } @@ -843,6 +858,8 @@ final class NodeAppModel { self.voiceWake.setSuppressedForBackground(true) self.talkMode.suspendForBackground() } + // Every TalkMode terminal path reports the exact capture after audio + // teardown; this callback is the single owner of Voice Wake lease release. self.talkMode.setPushToTalkAudioOwnershipEndHandler { [weak self] captureId in self?.releasePttVoiceWakeLease(for: captureId) } @@ -1046,10 +1063,11 @@ final class NodeAppModel { // schedule Voice Wake, which the background suspension must catch. self.voiceNoteRecorder.cancel() } - // Invalidate queued or permission-suspended PTT starts before releasing - // Talk. Its capture-end callback can otherwise restart Voice Wake after - // the background suspension has already run. + // Backgrounding invalidates both remote PTT and local dictation preparation. + // Route and PTT events only advance the remote epoch so they cannot cancel + // transcription-only permission prompts. self.talkPttCommandEpoch &+= 1 + self.chatDictationCommandEpoch &+= 1 let shouldKeepTalkActive = keepTalkActive && self.talkMode.canKeepContinuousTalkActiveInBackground self.backgroundTalkKeptActive = shouldKeepTalkActive self.talkMode.suspendForBackground(keepActive: shouldKeepTalkActive) @@ -1241,8 +1259,17 @@ final class NodeAppModel { self.voiceWake.setSuppressedByTalk(true) } else { self.voiceWake.setSuppressedByTalk(false) + self.cancelTalkPermissionUpgrade( + resumeOperatorGateway: self.forceOperatorTalkPermissionUpgradeRequest) } self.talkMode.setEnabled(enabled) + if enabled { + Task { [weak self] in + guard let self else { return } + await self.talkMode.reloadConfig() + self.requestTalkPermissionUpgradeIfNeeded() + } + } Task { [weak self] in await self?.pushTalkModeToGateway( enabled: enabled, @@ -1262,12 +1289,13 @@ final class NodeAppModel { self.talkMode.applyProviderSelectionChanged() } - func requestTalkPermissionUpgrade() { + private func requestTalkPermissionUpgrade() { guard let config = activeGatewayConnectConfig else { self.talkMode.gatewayTalkPermissionState = .requestFailed("Gateway is not connected") self.talkMode.statusText = "Gateway not connected" return } + guard !self.forceOperatorTalkPermissionUpgradeRequest else { return } GatewayDiagnostics.log("talk permission upgrade requested") self.talkMode.gatewayTalkPermissionState = .requestingUpgrade self.talkMode.statusText = "Requesting Talk approval" @@ -1278,33 +1306,108 @@ final class NodeAppModel { self.lastGatewayProblem = nil self.nodeGatewayProblem = nil self.operatorGatewayProblem = nil + self.lastTalkPermissionReconnectAttemptAt = Date() + self.restartOperatorGatewayForTalkPermissionUpgrade(config) + self.startTalkPermissionUpgradePolling() + } + + private func restartOperatorGatewayForTalkPermissionUpgrade(_ config: GatewayConnectConfig) { self.operatorGatewayTask?.cancel() self.operatorGatewayTask = nil let sessionBox = config.tls.map { WebSocketSessionBox(session: GatewayTLSPinningSession(params: $0)) } - Task { [weak self] in + let routeGeneration = self.gatewayRouteGeneration + self.talkPermissionUpgradeReconnectGeneration &+= 1 + let reconnectGeneration = self.talkPermissionUpgradeReconnectGeneration + self.talkPermissionUpgradeReconnectTask?.cancel() + self.talkPermissionUpgradeReconnectTask = Task { @MainActor [weak self] in guard let self else { return } + defer { self.finishTalkPermissionUpgradeReconnect(generation: reconnectGeneration) } await self.operatorGateway.disconnect() - await MainActor.run { - self.startOperatorGatewayLoop( - url: config.url, - stableID: config.effectiveStableID, - token: config.token, - bootstrapToken: config.bootstrapToken, - password: config.password, - nodeOptions: config.nodeOptions, - sessionBox: sessionBox) + guard !Task.isCancelled, + self.talkMode.isEnabled, + self.forceOperatorTalkPermissionUpgradeRequest, + self.isCurrentGatewayRoute( + generation: routeGeneration, + stableID: config.effectiveStableID) + else { return } + self.startOperatorGatewayLoop( + url: config.url, + stableID: config.effectiveStableID, + token: config.token, + bootstrapToken: config.bootstrapToken, + password: config.password, + nodeOptions: config.nodeOptions, + sessionBox: sessionBox) + } + } + + private func requestTalkPermissionUpgradeIfNeeded() { + guard self.talkMode.isEnabled, + case .missingScope = self.talkMode.gatewayTalkPermissionState + else { return } + self.requestTalkPermissionUpgrade() + } + + private func startTalkPermissionUpgradePolling() { + self.talkPermissionUpgradeTask?.cancel() + self.talkPermissionUpgradeTask = Task { @MainActor [weak self] in + while !Task.isCancelled { + try? await Task.sleep(nanoseconds: 3_000_000_000) + guard !Task.isCancelled, let self else { return } + await self.pollTalkPermissionUpgrade() + guard self.talkMode.gatewayTalkPermissionState.isApprovalRequestInProgress else { return } } } } - func pollTalkPermissionUpgrade() async { - guard self.talkMode.gatewayTalkPermissionState.isApprovalRequestInProgress else { - await self.talkMode.reloadConfig() - await self.talkMode.prefetchRealtimeSessionIfReady(reason: "talk_permission_poll") - return - } + private func cancelTalkPermissionUpgrade(resumeOperatorGateway: Bool = false) { + self.forceOperatorTalkPermissionUpgradeRequest = false + self.talkPermissionUpgradeTask?.cancel() + self.talkPermissionUpgradeTask = nil + self.talkPermissionUpgradeReconnectGeneration &+= 1 + self.talkPermissionUpgradeReconnectTask?.cancel() + self.talkPermissionUpgradeReconnectTask = nil + self.lastTalkPermissionReconnectAttemptAt = nil + guard resumeOperatorGateway, let config = self.activeGatewayConnectConfig else { return } - guard let cfg = activeGatewayConnectConfig else { + // A scope-upgrade attempt can pause the shared operator reconnect loop for approval. + // Cancelling Talk must replace that attempt with the normal stored-auth connection. + self.gatewayAutoReconnectEnabled = true + self.gatewayPairingPaused = false + self.gatewayPairingRequestId = nil + self.clearOperatorGatewayConnectionProblemIfCurrent() + self.operatorGatewayTask?.cancel() + self.operatorGatewayTask = nil + let sessionBox = config.tls.map { WebSocketSessionBox(session: GatewayTLSPinningSession(params: $0)) } + let routeGeneration = self.gatewayRouteGeneration + self.talkPermissionUpgradeReconnectGeneration &+= 1 + let reconnectGeneration = self.talkPermissionUpgradeReconnectGeneration + self.talkPermissionUpgradeReconnectTask = Task { @MainActor [weak self] in + guard let self else { return } + defer { self.finishTalkPermissionUpgradeReconnect(generation: reconnectGeneration) } + await self.operatorGateway.disconnect() + guard !Task.isCancelled, + self.isCurrentGatewayRoute( + generation: routeGeneration, + stableID: config.effectiveStableID) + else { return } + self.startOperatorGatewayLoop( + url: config.url, + stableID: config.effectiveStableID, + token: config.token, + bootstrapToken: config.bootstrapToken, + password: config.password, + nodeOptions: config.nodeOptions, + sessionBox: sessionBox) + } + } + + private func pollTalkPermissionUpgrade() async { + guard self.talkMode.gatewayTalkPermissionState.isApprovalRequestInProgress, + self.operatorTalkHydrationGeneration == nil + else { return } + guard let config = activeGatewayConnectConfig else { + self.forceOperatorTalkPermissionUpgradeRequest = false self.talkMode.gatewayTalkPermissionState = .requestFailed("Gateway is not connected") self.talkMode.statusText = "Gateway not connected" return @@ -1316,31 +1419,30 @@ final class NodeAppModel { { return } - lastTalkPermissionReconnectAttemptAt = now - + // The operator loop owns handshake retries. Polling only revives a loop + // that paused for approval; it must never impose a timeout on an active attempt. + guard Self.shouldRestartTalkPermissionUpgradePoll( + hasOperatorGatewayTask: self.operatorGatewayTask != nil, + hasReconnectTask: self.talkPermissionUpgradeReconnectTask != nil) + else { return } GatewayDiagnostics.log("talk permission approval poll reconnect") + self.lastTalkPermissionReconnectAttemptAt = now self.gatewayAutoReconnectEnabled = true self.gatewayPairingPaused = false self.gatewayPairingRequestId = nil - ensureOperatorReconnectLoopIfNeeded() + self.restartOperatorGatewayForTalkPermissionUpgrade(config) + } - if self.operatorGatewayTask == nil { - let sessionBox = cfg.tls.map { WebSocketSessionBox(session: GatewayTLSPinningSession(params: $0)) } - startOperatorGatewayLoop( - url: cfg.url, - stableID: cfg.effectiveStableID, - token: cfg.token, - bootstrapToken: cfg.bootstrapToken, - password: cfg.password, - nodeOptions: cfg.nodeOptions, - sessionBox: sessionBox) - } + nonisolated static func shouldRestartTalkPermissionUpgradePoll( + hasOperatorGatewayTask: Bool, + hasReconnectTask: Bool) -> Bool + { + !hasOperatorGatewayTask && !hasReconnectTask + } - guard await waitForOperatorConnection(timeoutMs: 2500, pollMs: 250) else { - return - } - await self.talkMode.reloadConfig() - await self.talkMode.prefetchRealtimeSessionIfReady(reason: "talk_permission_poll_connected") + private func finishTalkPermissionUpgradeReconnect(generation: UInt64) { + guard self.talkPermissionUpgradeReconnectGeneration == generation else { return } + self.talkPermissionUpgradeReconnectTask = nil } func setTalkSpeakerphoneEnabled(_ enabled: Bool) { @@ -2699,7 +2801,9 @@ final class NodeAppModel { let commandEpoch = self.talkPttCommandEpoch var reservedCaptureId: String? do { - let payload = try await self.withTalkCapturePreparation(commandEpoch: commandEpoch) { + let payload = try await self.withTalkCapturePreparation( + owner: .remotePushToTalk(epoch: commandEpoch)) + { try self.rejectTalkCaptureWhileOtherAudioActive() return try await self.talkMode.beginPushToTalk( canStartCapture: { @@ -2730,7 +2834,9 @@ final class NodeAppModel { var reservedCaptureId: String? let start: TalkPushToTalkOnceStart do { - start = try await self.withTalkCapturePreparation(commandEpoch: commandEpoch) { + start = try await self.withTalkCapturePreparation( + owner: .remotePushToTalk(epoch: commandEpoch)) + { try self.rejectTalkCaptureWhileOtherAudioActive() return try await self.talkMode.beginPushToTalkOnce( canStartCapture: { @@ -2759,12 +2865,12 @@ final class NodeAppModel { // Interrupt commands invalidate suspended preparation before touching // capture state, then bypass the preparation queue entirely. self.talkPttCommandEpoch &+= 1 - let payload = self.talkMode.endPushToTalk() + let payload = self.talkMode.endPushToTalk(expectedTranscriptionOnly: false) let json = try Self.encodePayload(payload) return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: json) case OpenClawTalkCommand.pttCancel.rawValue: self.talkPttCommandEpoch &+= 1 - let payload = self.talkMode.cancelPushToTalk() + let payload = self.talkMode.cancelPushToTalk(expectedTranscriptionOnly: false) let json = try Self.encodePayload(payload) return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: json) default: @@ -2775,6 +2881,85 @@ final class NodeAppModel { } } + /// Captures one locally recognized utterance for the chat draft. Unlike + /// remote PTT, this returns text without starting an agent turn or TTS. + func transcribeChatDraft() async throws -> String? { + guard self.chatDictationCaptureId == nil else { return nil } + + let commandEpoch = self.chatDictationCommandEpoch + var reservedCaptureId: String? + let start: TalkPushToTalkOnceStart + do { + start = try await self.withTalkCapturePreparation( + owner: .chatDictation(epoch: commandEpoch)) + { + try self.rejectTalkCaptureWhileOtherAudioActive() + return try await self.talkMode.beginPushToTalkOnce( + maxDurationSeconds: 30, + transcriptionOnly: true, + canStartCapture: { + self.chatDictationCommandEpoch == commandEpoch && !self.isBackgrounded + }, + onCaptureReserved: { captureId in + reservedCaptureId = captureId + self.chatDictationCaptureId = captureId + self.acquirePttVoiceWakeLease(for: captureId) + }) + } + } catch { + self.cancelChatDictationReservation(reservedCaptureId) + throw error + } + + guard case let .started(captureId) = start else { + self.cancelChatDictationReservation(reservedCaptureId) + return nil + } + guard reservedCaptureId == captureId else { + self.cancelChatDictationReservation(reservedCaptureId) + if reservedCaptureId != captureId { + _ = self.talkMode.cancelPushToTalk(captureId: captureId) + } + return nil + } + + let payload = await self.talkMode.awaitPushToTalkOnce(start) + // Dictation may be cancelled while recognition is suspended. Only the + // invocation that still owns this capture may publish its transcript. + guard payload.captureId == captureId, + self.chatDictationCaptureId == captureId + else { + if self.chatDictationCaptureId == captureId { + self.chatDictationCaptureId = nil + } + return nil + } + self.chatDictationCaptureId = nil + return payload.transcript?.trimmingCharacters(in: .whitespacesAndNewlines) + } + + private func cancelChatDictationReservation(_ captureId: String?) { + guard let captureId else { return } + _ = self.talkMode.cancelPushToTalk(captureId: captureId) + if self.chatDictationCaptureId == captureId { + self.chatDictationCaptureId = nil + } + } + + func finishChatDictation() { + guard let captureId = self.chatDictationCaptureId else { return } + _ = self.talkMode.endPushToTalk(captureId: captureId) + } + + func cancelChatDictation() { + // Cancellation must also invalidate permission/preparation work before a + // capture ID exists, or a dismissed dictation can start recording later. + self.chatDictationCommandEpoch &+= 1 + guard let captureId = self.chatDictationCaptureId else { return } + _ = self.talkMode.cancelPushToTalk(captureId: captureId) + self.chatDictationCaptureId = nil + } + private func rejectTalkCaptureWhileOtherAudioActive() throws { // Remote PTT bypasses the Chat Talk toggle. Preserve the user's draft; // Talk must not reconfigure AVAudioSession while its recorder owns it. @@ -2866,21 +3051,35 @@ final class NodeAppModel { } private func withTalkCapturePreparation( - commandEpoch: UInt64, + owner: TalkCapturePreparationOwner, operation: () async throws -> T) async throws -> T { try await self.acquireTalkPreparation() defer { self.releaseTalkPreparation() } - try self.ensureTalkPttCommandCurrent(commandEpoch) + try self.ensureTalkCapturePreparationCurrent(owner) #if DEBUG if let testTalkCapturePreparationHandler { await testTalkCapturePreparationHandler() } #endif - try self.ensureTalkPttCommandCurrent(commandEpoch) + try self.ensureTalkCapturePreparationCurrent(owner) return try await operation() } + private func ensureTalkCapturePreparationCurrent(_ owner: TalkCapturePreparationOwner) throws { + switch owner { + case let .remotePushToTalk(epoch): + try self.ensureTalkPttCommandCurrent(epoch) + case let .chatDictation(epoch): + try Task.checkCancellation() + guard self.chatDictationCommandEpoch == epoch, !self.isBackgrounded else { + throw NSError(domain: "TalkMode", code: 9, userInfo: [ + NSLocalizedDescriptionKey: "DICTATION_CANCELLED: chat dictation start was cancelled", + ]) + } + } + } + private func ensureTalkPttCommandCurrent(_ commandEpoch: UInt64) throws { try Task.checkCancellation() guard self.talkPttCommandEpoch == commandEpoch, !self.isBackgrounded else { @@ -3693,6 +3892,7 @@ extension NodeAppModel { self.lastGatewayProblem = nil self.nodeGatewayProblem = nil self.operatorGatewayProblem = nil + self.cancelTalkPermissionUpgrade() // Publish teardown through the shared barrier before returning. A replacement connect // must await old loop cleanup instead of racing this synchronous UI action. _ = self.beginGatewaySessionReset(chainingAfterExisting: true) @@ -3744,6 +3944,7 @@ extension NodeAppModel { { self.invalidateNodePushToTalkRoute() self.operatorTalkConnectionGeneration &+= 1 + self.operatorTalkHydrationGeneration = nil self.chatSessionRoutingRestoreTask?.cancel() self.isAppleReviewDemoModeEnabled = false self.isScreenshotFixtureModeEnabled = false @@ -4172,9 +4373,14 @@ extension NodeAppModel { else { return } self.operatorTalkConnectionGeneration &+= 1 let talkConnectionGeneration = self.operatorTalkConnectionGeneration + self.operatorTalkHydrationGeneration = talkConnectionGeneration + defer { + if self.operatorTalkHydrationGeneration == talkConnectionGeneration { + self.operatorTalkHydrationGeneration = nil + } + } self.setOperatorConnected(true) self.clearOperatorGatewayConnectionProblemIfCurrent() - self.forceOperatorTalkPermissionUpgradeRequest = false GatewayDiagnostics.log( "operator gateway connected host=\(url.host ?? "?") scheme=\(url.scheme ?? "?")") @@ -4196,6 +4402,16 @@ extension NodeAppModel { guard shouldContinue() else { return } await self.talkMode.reloadConfig(shouldApply: shouldContinue) guard shouldContinue() else { return } + self.requestTalkPermissionUpgradeIfNeeded() + if self.forceOperatorTalkPermissionUpgradeRequest { + guard !self.talkMode.gatewayTalkPermissionState.requiresTalkPermissionAction else { return } + // Completing the forced handshake must also release any approval pause so the + // shared operator route keeps reconnecting after this successful connection. + self.gatewayAutoReconnectEnabled = true + self.gatewayPairingPaused = false + self.gatewayPairingRequestId = nil + self.cancelTalkPermissionUpgrade() + } self.admitTalkAfterSessionHydration() await self.talkMode.prefetchRealtimeSessionIfReady( reason: "operator_connected", @@ -4432,7 +4648,7 @@ extension NodeAppModel { self.applyOperatorGatewayConnectionProblem(nextProblem) } if talkPermissionUpgradeRequest, nextProblem.kind == .pairingScopeUpgradeRequired { - self.talkMode.markTalkPermissionUpgradeRequested(requestId: nextProblem.requestId) + self.talkMode.markTalkPermissionUpgradeRequested() } } return nextProblem @@ -4463,6 +4679,7 @@ extension NodeAppModel { private func invalidateOperatorTalkRoute() { self.operatorTalkConnectionGeneration &+= 1 + self.operatorTalkHydrationGeneration = nil // A socket replacement invalidates Talk, not gateway identity hydration. The // replacement connection must await the same restore before admitting capture. self.setOperatorConnected(false) @@ -4478,7 +4695,7 @@ extension NodeAppModel { private func invalidateNodePushToTalkRoute() { self.talkPttCommandEpoch &+= 1 self.voiceWake.invalidatePendingCommand() - _ = self.talkMode.cancelPushToTalk() + _ = self.talkMode.cancelPushToTalk(expectedTranscriptionOnly: false) } private func startNodeGatewayLoop( @@ -10252,6 +10469,14 @@ extension NodeAppModel { #if DEBUG extension NodeAppModel { + func _test_setActiveGatewayConnectConfig(_ config: GatewayConnectConfig?) { + self.activeGatewayConnectConfig = config + } + + func _test_forceTalkPermissionUpgradeRequest() -> Bool { + self.forceOperatorTalkPermissionUpgradeRequest + } + func _test_handleInvoke( _ req: BridgeInvokeRequest, gatewayStableID: String? = nil) async -> BridgeInvokeResponse diff --git a/apps/ios/Sources/Onboarding/OnboardingWizardView.swift b/apps/ios/Sources/Onboarding/OnboardingWizardView.swift index 72e7b7349e18..4fe4b13dfdf3 100644 --- a/apps/ios/Sources/Onboarding/OnboardingWizardView.swift +++ b/apps/ios/Sources/Onboarding/OnboardingWizardView.swift @@ -842,6 +842,8 @@ extension OnboardingWizardView { } } .font(OpenClawType.subheadSemiBold) + .frame(maxWidth: .infinity, alignment: .leading) + .contentShape(Rectangle()) .disabled(!self.canConnectManual || self.connectingGateway != nil) } diff --git a/apps/ios/Sources/OpenClawApp.swift b/apps/ios/Sources/OpenClawApp.swift index 981bb307ce6c..831d3632bd25 100644 --- a/apps/ios/Sources/OpenClawApp.swift +++ b/apps/ios/Sources/OpenClawApp.swift @@ -672,6 +672,7 @@ struct OpenClawApp: App { @State private var appearanceModel: AppAppearanceModel @State private var appModel: NodeAppModel @State private var gatewayController: GatewayConnectionController + @State private var voiceLiveActivityCoordinator: VoiceLiveActivityCoordinator @UIApplicationDelegateAdaptor(OpenClawAppDelegate.self) private var appDelegate @Environment(\.scenePhase) private var scenePhase @@ -701,6 +702,7 @@ struct OpenClawApp: App { OpenClawAppModelRegistry.appModel = appModel _appearanceModel = State(initialValue: AppAppearanceModel()) _appModel = State(initialValue: appModel) + _voiceLiveActivityCoordinator = State(initialValue: VoiceLiveActivityCoordinator()) _gatewayController = State( initialValue: GatewayConnectionController( appModel: appModel, @@ -719,11 +721,17 @@ struct OpenClawApp: App { .environment(self.appModel.voiceWake) .environment(self.gatewayController) .task { + self.voiceLiveActivityCoordinator.start(appModel: self.appModel) self.appModel.setScenePhase(self.scenePhase) self.appDelegate.appModel = self.appModel self.appDelegate.scenePhaseChanged(self.scenePhase) self.applyWindowTint() self.gatewayController.setScenePhase(self.scenePhase) + #if DEBUG + if Self.liveActivityVoicePreviewEnabled { + LiveActivityManager.shared.startVoicePreview() + } + #endif } .onReceive( NotificationCenter.default.publisher(for: UIContentSizeCategory.didChangeNotification), @@ -760,6 +768,14 @@ struct OpenClawApp: App { #endif } + private static var liveActivityVoicePreviewEnabled: Bool { + #if DEBUG + ProcessInfo.processInfo.arguments.contains("--openclaw-live-activity-voice-preview") + #else + false + #endif + } + @MainActor private func applyWindowTint() { for window in Self.connectedWindows() { diff --git a/apps/ios/Sources/RootTabs.swift b/apps/ios/Sources/RootTabs.swift index 01bc79344342..31e906226f74 100644 --- a/apps/ios/Sources/RootTabs.swift +++ b/apps/ios/Sources/RootTabs.swift @@ -8,6 +8,7 @@ struct RootTabs: View { @Environment(VoiceWakeManager.self) private var voiceWake @Environment(GatewayConnectionController.self) private var gatewayController @Environment(\.accessibilityReduceMotion) private var reduceMotion + @Environment(\.colorScheme) private var colorScheme @Environment(\.horizontalSizeClass) private var horizontalSizeClass @Environment(\.rootTabsUserInterfaceIdiomOverride) private var userInterfaceIdiomOverride @Environment(\.scenePhase) private var scenePhase @@ -66,10 +67,9 @@ struct RootTabs: View { switch arguments[valueIndex].lowercased() { case "control", "overview": return .control - case "chat": + // These shipped launch aliases now target the unified conversational surface. + case "chat", "talk", "voice": return .chat - case "talk", "voice": - return .talk case "agent", "agents": return .agent case "settings": @@ -153,65 +153,6 @@ struct RootTabs: View { } } - private var phoneTabContent: some View { - TabView(selection: self.phoneTabSelection) { - PhoneTabSettingsHost(resetRequestID: self.phoneChatSettingsResetRequestID) { openSettingsRoute in - ChatProTab( - headerLeadingAction: self.phoneChatReturnAction, - ownsNavigationStack: false, - openSettings: { openSettingsRoute(.gateway) }) - } - .tabItem { Label("Chat", systemImage: "bubble.left.fill") } - .tag(AppTab.chat) - - PhoneTabSettingsHost { openSettingsRoute in - TalkProTab( - ownsNavigationStack: false, - openSettings: { openSettingsRoute(.gateway) }, - openVoiceSettings: { openSettingsRoute(.voice) }) - } - .tabItem { - Label( - "Talk", - systemImage: self.appModel.talkMode.isEnabled ? "waveform.circle.fill" : "waveform.circle") - .font(OpenClawType.captionSemiBold) - } - .tag(AppTab.talk) - - RootTabsPhoneControlHub( - groups: Self.phoneControlGroups, - initialDestination: Self.requestedInitialSidebarDestination, - navigationRequest: self.phoneControlNavigationRequest, - openRootDestination: { self.selectSidebarDestination($0) }, - openChatFromControlDetail: { self.openChatFromControlDetail($0) }) - .tabItem { Label("Control", systemImage: "square.grid.2x2") } - .badge(self.appModel.pendingExecApprovalCount) - .tag(AppTab.control) - - PhoneTabSettingsHost { openSettingsRoute in - AgentProTab( - directRoute: .agents, - openSettings: { openSettingsRoute(.gateway) }) - } - .tabItem { Label("Agent", systemImage: "person.2.fill") } - .tag(AppTab.agent) - - SettingsProTab( - initialRoute: self.selectedSettingsRoute, - acceptsGatewaySetupRequests: !self.showOnboarding && - self.selectedTab == .settings && - self.selectedSettingsRoute == .gateway, - onRouteChange: self.handleSettingsRouteChange, - onApprovalNotificationsRoute: self.suppressExecApprovalPromptForNotificationSettings, - gatewaySetupRequest: self.gatewaySetupRequest, - onGatewaySetupRequestHandled: self.handleGatewaySetupRequest) - .id(self.settingsTabViewID) - .tabItem { Label("Settings", systemImage: "gearshape.fill") } - .tag(AppTab.settings) - } - .openClawTabBarBehavior() - } - private var sidebarSplitContent: some View { GeometryReader { proxy in let isDrawerLayout = self.shouldUseSidebarDrawer(containerSize: proxy.size) @@ -433,12 +374,6 @@ struct RootTabs: View { showsAgentBadge: false, ownsNavigationStack: false, openSettings: { self.selectSidebarDestination(.gateway) }) - case .talk: - TalkProTab( - headerLeadingAction: self.sidebarHeaderLeadingAction, - ownsNavigationStack: false, - openSettings: { self.selectSidebarDestination(.gateway) }, - openVoiceSettings: { self.selectSettingsRoute(.voice) }) case .overview: CommandCenterTab( ownsNavigationStack: false, @@ -959,6 +894,71 @@ struct RootTabs: View { } } +extension RootTabs { + private var phoneTabContent: some View { + TabView(selection: self.phoneTabSelection) { + PhoneTabSettingsHost(resetRequestID: self.phoneChatSettingsResetRequestID) { openSettingsRoute in + ChatProTab( + headerLeadingAction: self.phoneChatReturnAction, + ownsNavigationStack: false, + openSettings: { openSettingsRoute(.gateway) }) + } + .tabItem { + Label { + Text("Chat") + .font(OpenClawType.caption) + } icon: { + UnifiedChatVoiceTabIcon.image( + state: self.phoneChatVoiceIconState, + colorScheme: self.colorScheme) + } + .accessibilityLabel( + self.appModel.talkMode.isEnabled ? "Chat, voice active" : "Chat") + } + .tag(AppTab.chat) + + RootTabsPhoneControlHub( + groups: Self.phoneControlGroups, + initialDestination: self.selectedSidebarDestination, + navigationRequest: self.phoneControlNavigationRequest, + openRootDestination: { self.selectSidebarDestination($0) }, + openChatFromControlDetail: { self.openChatFromControlDetail($0) }) + .tabItem { Label("Control", systemImage: "square.grid.2x2") } + .badge(self.appModel.pendingExecApprovalCount) + .tag(AppTab.control) + + PhoneTabSettingsHost { openSettingsRoute in + AgentProTab( + directRoute: .agents, + openSettings: { openSettingsRoute(.gateway) }) + } + .tabItem { Label("Agent", systemImage: "person.2.fill") } + .tag(AppTab.agent) + + SettingsProTab( + initialRoute: self.selectedSettingsRoute, + acceptsGatewaySetupRequests: !self.showOnboarding && + self.selectedTab == .settings && + self.selectedSettingsRoute == .gateway, + onRouteChange: self.handleSettingsRouteChange, + onApprovalNotificationsRoute: self.suppressExecApprovalPromptForNotificationSettings, + gatewaySetupRequest: self.gatewaySetupRequest, + onGatewaySetupRequestHandled: self.handleGatewaySetupRequest) + .id(self.settingsTabViewID) + .tabItem { Label("Settings", systemImage: "gearshape.fill") } + .tag(AppTab.settings) + } + .openClawTabBarBehavior() + } + + private var phoneChatVoiceIconState: UnifiedChatVoiceTabIcon.State { + guard self.appModel.talkMode.isEnabled else { return .inactive } + if self.appModel.talkMode.isSpeaking { return .speaking } + if self.appModel.talkMode.isListening { return .listening } + return .active + } +} + extension RootTabs { private func updateCanvasState() { self.updateHomeCanvasState() @@ -1390,59 +1390,6 @@ extension RootTabs { } } -/// Phone tabs push Settings routes (gateway, voice) onto their own stack so -/// Back returns to the tab content the user navigated from; only global flows -/// (deep links, onboarding, problem banner) jump to the canonical Settings tab. -private struct PhoneTabSettingsHost: View { - @State private var settingsPath: [SettingsRoute] = [] - private let resetRequestID: Int - private let content: (_ openSettingsRoute: @escaping (SettingsRoute) -> Void) -> Content - - init( - resetRequestID: Int = 0, - @ViewBuilder content: @escaping (_ openSettingsRoute: @escaping (SettingsRoute) -> Void) -> Content) - { - self.resetRequestID = resetRequestID - self.content = content - } - - var body: some View { - NavigationStack(path: self.$settingsPath) { - self.content { route in - self.settingsPath.append(route) - } - .navigationDestination(for: SettingsRoute.self) { route in - SettingsProTab(directRoute: route) - } - } - .onChange(of: self.resetRequestID) { _, _ in - self.settingsPath.removeAll() - } - } -} - -private struct RootTabsHomeCanvasPayload: Codable { - var gatewayState: String - var eyebrow: String - var title: String - var subtitle: String - var gatewayLabel: String - var activeAgentName: String - var activeAgentBadge: String - var activeAgentCaption: String - var agentCount: Int - var agents: [RootTabsHomeCanvasAgentCard] - var footer: String -} - -private struct RootTabsHomeCanvasAgentCard: Codable { - var id: String - var name: String - var badge: String - var caption: String - var isActive: Bool -} - private struct RootCameraFlashOverlay: View { var nonce: Int diff --git a/apps/ios/Sources/RootTabsCanvasPayload.swift b/apps/ios/Sources/RootTabsCanvasPayload.swift new file mode 100644 index 000000000000..f25c1977ba3c --- /dev/null +++ b/apps/ios/Sources/RootTabsCanvasPayload.swift @@ -0,0 +1,21 @@ +struct RootTabsHomeCanvasPayload: Codable { + var gatewayState: String + var eyebrow: String + var title: String + var subtitle: String + var gatewayLabel: String + var activeAgentName: String + var activeAgentBadge: String + var activeAgentCaption: String + var agentCount: Int + var agents: [RootTabsHomeCanvasAgentCard] + var footer: String +} + +struct RootTabsHomeCanvasAgentCard: Codable { + var id: String + var name: String + var badge: String + var caption: String + var isActive: Bool +} diff --git a/apps/ios/Sources/RootTabsNavigation.swift b/apps/ios/Sources/RootTabsNavigation.swift index d499ae223dca..69b47e744b67 100644 --- a/apps/ios/Sources/RootTabsNavigation.swift +++ b/apps/ios/Sources/RootTabsNavigation.swift @@ -31,14 +31,12 @@ extension RootTabs { enum AppTab: Hashable { case control case chat - case talk case agent case settings } enum SidebarDestination: String, CaseIterable, Hashable, Identifiable { case chat - case talk case overview case activity case agents @@ -62,7 +60,6 @@ extension RootTabs { var title: String { switch self { case .chat: String(localized: "Chat") - case .talk: String(localized: "Talk") case .overview: String(localized: "Overview") case .activity: String(localized: "Activity") case .agents: String(localized: "Agents") @@ -91,7 +88,6 @@ extension RootTabs { var systemImage: String { switch self { case .chat: "bubble.left" - case .talk: "waveform.circle" case .overview: "chart.bar" case .activity: "waveform.path.ecg" case .agents: "person.2" @@ -114,8 +110,6 @@ extension RootTabs { switch self { case .chat: .chat - case .talk: - .talk case .agents: .agent case .settings, .gateway: @@ -132,7 +126,7 @@ extension RootTabs { switch self { case .gateway: .gateway - case .chat, .talk, .overview, .activity, .agents, .workboard, .skillWorkshop, .instances, .sessions, + case .chat, .overview, .activity, .agents, .workboard, .skillWorkshop, .instances, .sessions, .files, .dreaming, .usage, .cron, .terminal, .settings, .docs: @@ -209,7 +203,7 @@ extension RootTabs { static func shouldOpenRootTabFromPhoneHub(_ destination: SidebarDestination) -> Bool { switch destination { - case .chat, .talk, .agents, .gateway, .settings: + case .chat, .agents, .gateway, .settings: true case .overview, .activity, .workboard, .skillWorkshop, .instances, .sessions, .files, .dreaming, @@ -225,8 +219,6 @@ extension RootTabs { .overview case .chat: .chat - case .talk: - .talk case .agent: .agents case .settings: @@ -287,7 +279,7 @@ extension RootTabs { } static let sidebarGroups: [SidebarGroup] = [ - SidebarGroup(title: "CHAT", destinations: [.chat, .talk]), + SidebarGroup(title: "CHAT", destinations: [.chat]), SidebarGroup( title: "CONTROL", destinations: [ @@ -311,8 +303,7 @@ extension RootTabs { ] static var phoneControlGroups: [SidebarGroup] { - // Agents owns a bottom tab and its hub entry duplicated the same destination; - // Chat and Talk stay per the tested Control-hub IA contract. + // Agents owns a bottom tab and its hub entry duplicated the same destination. let tabOwned: Set = [.agents] return self.sidebarGroups .map { group in diff --git a/apps/ios/Sources/Voice/TalkGatewayPermissionState.swift b/apps/ios/Sources/Voice/TalkGatewayPermissionState.swift index f4120bc81a99..14193e766bec 100644 --- a/apps/ios/Sources/Voice/TalkGatewayPermissionState.swift +++ b/apps/ios/Sources/Voice/TalkGatewayPermissionState.swift @@ -5,7 +5,7 @@ enum TalkGatewayPermissionState: Equatable { case ready case missingScope(String) case requestingUpgrade - case upgradeRequested(requestId: String?) + case upgradeRequested case requestFailed(String) case apiKeyMissing case loadFailed(String) @@ -48,18 +48,4 @@ enum TalkGatewayPermissionState: Equatable { false } } - - var failureMessage: String? { - if case let .requestFailed(message) = self { - return message - } - return nil - } - - var requestId: String? { - if case let .upgradeRequested(requestId) = self { - return requestId - } - return nil - } } diff --git a/apps/ios/Sources/Voice/TalkModeManager.swift b/apps/ios/Sources/Voice/TalkModeManager.swift index 31a1880c4a6c..8e8c90f7689f 100644 --- a/apps/ios/Sources/Voice/TalkModeManager.swift +++ b/apps/ios/Sources/Voice/TalkModeManager.swift @@ -55,7 +55,8 @@ private struct FinishingPushToTalk { private struct ActivePushToTalk { let captureId: String - let gatewayContext: PushToTalkGatewayContext + let gatewayContext: PushToTalkGatewayContext? + let transcriptionOnly: Bool } private enum ChatCompletionState { @@ -387,7 +388,7 @@ final class TalkModeManager: NSObject { if self.realtimeSessionReadyAt == nil { self.realtimeSessionReadyAt = Date() } - self.markRealtimeActive() + markRealtimeActive() } private func scheduleRealtimeRestart(after delayNanoseconds: UInt64?, generation: Int) { @@ -458,7 +459,7 @@ final class TalkModeManager: NSObject { activeDuration: activeDuration) self.rapidRealtimeRestartCount = attempt guard let delay = Self.realtimeRestartDelayNanoseconds(attempt: attempt) else { - let issue = self.realtimeIssue( + let issue = realtimeIssue( message: "Realtime disconnected repeatedly.", phase: "reconnect") self.pendingRealtimeIssue = issue @@ -484,7 +485,11 @@ final class TalkModeManager: NSObject { func attachGateway(_ gateway: GatewayNodeSession) { if let current = self.gateway, current !== gateway { - if let captureId = self.activePTTCaptureId { + // Local dictation has no gateway owner, so a route replacement must + // not discard an in-progress draft capture. + if let captureId = activePTTCaptureId, + self.activePushToTalk?.transcriptionOnly != true + { _ = self.cancelPushToTalk(captureId: captureId) } self.cancelFinishingPushToTalk() @@ -502,20 +507,23 @@ final class TalkModeManager: NSObject { } } else { self.cancelPendingStart() - if let captureId = self.activePTTCaptureId { + let preservesLocalTranscription = self.activePushToTalk?.transcriptionOnly == true + if let captureId = activePTTCaptureId, !preservesLocalTranscription { _ = self.cancelPushToTalk(captureId: captureId) } self.cancelFinishingPushToTalk() self.resetRealtimeRestartState() self.stopRealtimeSession() - self.stopNativeCaptureAndDiscardTranscript() - self.stopSpeaking(storeInterruption: false) - self.deactivateAudioSession() + if !preservesLocalTranscription { + self.stopNativeCaptureAndDiscardTranscript() + self.stopSpeaking(storeInterruption: false) + deactivateAudioSession() + if self.isEnabled, !self.isSpeaking { + self.setStatus(String(localized: "Offline"), phase: .idle) + } + } self.gatewayTalkActiveModeTitle = String(localized: "Not active") self.gatewayTalkActiveModeSubtitle = nil - if self.isEnabled, !self.isSpeaking { - self.setStatus(String(localized: "Offline"), phase: .idle) - } self.realtimePrefetchGeneration &+= 1 self.realtimePrefetchTask?.cancel() self.realtimePrefetchTask = nil @@ -527,10 +535,12 @@ final class TalkModeManager: NSObject { func updateMainSessionKey(_ sessionKey: String?) -> Bool { let trimmed = (sessionKey ?? "").trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { return false } - if trimmed == self.mainSessionKey { return false } + if trimmed == self.mainSessionKey { + return false + } let hasTalkOwner = self.hasRealtimeOwnerOrStart || self.hasContinuousTalkOwner let shouldRestartTalk = self.isEnabled && hasTalkOwner - if let captureId = self.activePTTCaptureId { + if let captureId = activePTTCaptureId { _ = self.cancelPushToTalk(captureId: captureId) } self.cancelFinishingPushToTalk() @@ -541,7 +551,7 @@ final class TalkModeManager: NSObject { self.resetRealtimeRestartState() self.stopRealtimeSession() self.stopNativeCaptureAndDiscardTranscript() - self.deactivateAudioSession() + deactivateAudioSession() } self.mainSessionKey = trimmed if shouldRestartTalk, self.gatewayConnected, self.isEnabled { @@ -618,9 +628,9 @@ final class TalkModeManager: NSObject { if let realtimeSession { try realtimeSession.applyAudioRoutePreferenceChanged() } else if self.realtimeRelaySession != nil { - try self.configureOwnedRealtimeAudioSession() + try configureOwnedRealtimeAudioSession() } else { - try self.configureOwnedAudioSession() + try configureOwnedAudioSession() } } catch { GatewayDiagnostics.log("talk audio route preference failed error=\(error.localizedDescription)") @@ -642,7 +652,7 @@ final class TalkModeManager: NSObject { } } #if DEBUG - if let testStartEntryHandler = self.testStartEntryHandler { + if let testStartEntryHandler { await testStartEntryHandler() guard self.isCurrentStartAttempt(attemptID) else { return } } @@ -667,7 +677,7 @@ final class TalkModeManager: NSObject { return } guard self.isCurrentStartAttempt(attemptID) else { return } - await self.ensureTalkConfigLoadedForStart() + await ensureTalkConfigLoadedForStart() guard self.isCurrentStartAttempt(attemptID) else { return } if self.gatewayTalkPermissionState.requiresTalkPermissionAction { self.setStatus(String(localized: "Gateway permission required"), phase: .idle) @@ -697,7 +707,7 @@ final class TalkModeManager: NSObject { guard speechOk else { self.logger.warning("start blocked: speech permission denied") self.stopNativeCaptureAndDiscardTranscript() - self.deactivateAudioSession() + deactivateAudioSession() let status = Self.permissionMessage( kind: String(localized: "Speech recognition"), status: SFSpeechRecognizer.authorizationStatus()) @@ -711,7 +721,7 @@ final class TalkModeManager: NSObject { do { GatewayDiagnostics.log("talk.timeline fallback speech pipeline start") - try self.configureOwnedAudioSession() + try configureOwnedAudioSession() // Set this before starting recognition so any early speech errors are classified correctly. self.captureMode = .continuous try self.startRecognition() @@ -725,7 +735,7 @@ final class TalkModeManager: NSObject { self.logger.info("listening") } catch { self.stopNativeCaptureAndDiscardTranscript() - self.deactivateAudioSession() + deactivateAudioSession() let status = String( format: String(localized: "Start failed: %@"), error.localizedDescription) @@ -841,7 +851,7 @@ final class TalkModeManager: NSObject { self.pttTimeoutTask?.cancel() self.pttTimeoutTask = nil self.pttAutoStopEnabled = false - if let pendingCaptureId, self.pttOnceOperations[pendingCaptureId] != nil { + if let pendingCaptureId, pttOnceOperations[pendingCaptureId] != nil { let payload = OpenClawTalkPTTStopPayload( captureId: pendingCaptureId, transcript: nil, @@ -852,7 +862,7 @@ final class TalkModeManager: NSObject { self.finishActivePushToTalk(pendingCaptureId) } TalkSystemSpeechSynthesizer.shared.stop() - self.deactivateAudioSession() + deactivateAudioSession() } /// Suspends microphone usage without disabling Talk Mode. @@ -903,13 +913,15 @@ final class TalkModeManager: NSObject { } TalkSystemSpeechSynthesizer.shared.stop() - self.deactivateAudioSession() + deactivateAudioSession() } func resumeAfterBackground(wasKeptActive: Bool = false) { self.foregroundPushToTalkAllowed = true self.foregroundAudioCaptureAllowed = true - if wasKeptActive, self.hasContinuousTalkOwner { return } + if wasKeptActive, self.hasContinuousTalkOwner { + return + } guard self.isEnabled else { return } Task { @MainActor [weak self] in await self?.start() @@ -917,6 +929,7 @@ final class TalkModeManager: NSObject { } func beginPushToTalk( + transcriptionOnly: Bool = false, canStartCapture: @MainActor () -> Bool = { true }, onCaptureReserved: @MainActor (String) -> Void = { _ in }) async throws -> OpenClawTalkPTTStartPayload { @@ -924,16 +937,21 @@ final class TalkModeManager: NSObject { guard canStartCapture(), self.foregroundPushToTalkAllowed else { throw Self.pushToTalkStartCancelledError() } - if self.isPushToTalkActive, let captureId = activePTTCaptureId { - return OpenClawTalkPTTStartPayload(captureId: captureId) + if self.isPushToTalkActive, let activePushToTalk { + guard activePushToTalk.transcriptionOnly == transcriptionOnly else { + throw Self.pushToTalkModeConflictError() + } + return OpenClawTalkPTTStartPayload(captureId: activePushToTalk.captureId) } if self.finishingPushToTalk != nil { throw Self.pushToTalkBusyError() } - guard self.gatewayConnected else { throw self.pushToTalkOfflineError() } + guard transcriptionOnly || self.gatewayConnected else { throw self.pushToTalkOfflineError() } - let gatewayContext: PushToTalkGatewayContext - if let gateway = self.gateway { + let gatewayContext: PushToTalkGatewayContext? + if transcriptionOnly { + gatewayContext = nil + } else if let gateway { let gatewayRoute = await gateway.currentRoute() try Task.checkCancellation() guard canStartCapture(), self.gatewayConnected, self.gateway === gateway else { @@ -961,7 +979,7 @@ final class TalkModeManager: NSObject { #endif } try Task.checkCancellation() - guard canStartCapture(), self.gatewayConnected, self.foregroundPushToTalkAllowed else { + guard canStartCapture(), transcriptionOnly || self.gatewayConnected, self.foregroundPushToTalkAllowed else { throw Self.pushToTalkStartCancelledError() } guard self.activePushToTalk == nil, self.finishingPushToTalk == nil else { @@ -987,7 +1005,8 @@ final class TalkModeManager: NSObject { let captureId = UUID().uuidString self.activePushToTalk = ActivePushToTalk( captureId: captureId, - gatewayContext: gatewayContext) + gatewayContext: gatewayContext, + transcriptionOnly: transcriptionOnly) // Reserve the capture mode before permission awaits so delayed continuous // starts cannot open a second microphone while PTT is preparing. self.captureMode = .pushToTalk @@ -997,42 +1016,18 @@ final class TalkModeManager: NSObject { do { #if DEBUG - if let testPTTReservedHandler = self.testPTTReservedHandler { + if let testPTTReservedHandler { await testPTTReservedHandler() try self.ensurePushToTalkStartCurrent(captureId: captureId, canStartCapture: canStartCapture) } #endif self.setStatus(String(localized: "Requesting permissions…"), phase: .connecting) - if !self.allowSimulatorCapture { - let micOk = await Self.requestMicrophonePermission() - try self.ensurePushToTalkStartCurrent(captureId: captureId, canStartCapture: canStartCapture) - guard micOk else { - self.setStatus( - String(localized: "Microphone permission denied"), - phase: .idle, - watchPresentation: .localized("Microphone permission denied")) - throw NSError(domain: "TalkMode", code: 4, userInfo: [ - NSLocalizedDescriptionKey: "Microphone permission denied", - ]) - } - let speechOk = await Self.requestSpeechPermission() - try self.ensurePushToTalkStartCurrent(captureId: captureId, canStartCapture: canStartCapture) - guard speechOk else { - let status = Self.permissionMessage( - kind: String(localized: "Speech recognition"), - status: SFSpeechRecognizer.authorizationStatus()) - self.setStatus( - status, - phase: .idle, - watchPresentation: .verbatim(status)) - throw NSError(domain: "TalkMode", code: 5, userInfo: [ - NSLocalizedDescriptionKey: "Speech recognition permission denied", - ]) - } - } + try await self.requestPushToTalkPermissions( + captureId: captureId, + canStartCapture: canStartCapture) try self.ensurePushToTalkStartCurrent(captureId: captureId, canStartCapture: canStartCapture) - try self.configureOwnedAudioSession() + try configureOwnedAudioSession() try self.ensurePushToTalkStartCurrent(captureId: captureId, canStartCapture: canStartCapture) self.captureMode = .pushToTalk try self.startRecognition(pttCaptureId: captureId) @@ -1076,8 +1071,20 @@ final class TalkModeManager: NSObject { return self.endPushToTalk(captureId: captureId) } + func endPushToTalk(expectedTranscriptionOnly: Bool) -> OpenClawTalkPTTStopPayload { + guard let activePushToTalk, + activePushToTalk.transcriptionOnly == expectedTranscriptionOnly + else { + return OpenClawTalkPTTStopPayload( + captureId: UUID().uuidString, + transcript: nil, + status: "idle") + } + return self.endPushToTalk(captureId: activePushToTalk.captureId) + } + func endPushToTalk(captureId: String) -> OpenClawTalkPTTStopPayload { - guard let activePushToTalk = self.activePushToTalk, + guard let activePushToTalk, activePushToTalk.captureId == captureId else { return OpenClawTalkPTTStopPayload(captureId: captureId, transcript: nil, status: "idle") @@ -1128,6 +1135,19 @@ final class TalkModeManager: NSObject { return payload } + if activePushToTalk.transcriptionOnly { + self.setStatus(String(localized: "Ready"), phase: .idle) + let shouldResume = self.isEnabled + self.finishActivePushToTalk(captureId) + let payload = OpenClawTalkPTTStopPayload( + captureId: captureId, + transcript: transcript, + status: "transcribed") + self.finishPTTOnce(payload) + self.scheduleContinuousResume(shouldResume) + return payload + } + guard self.gatewayConnected else { self.setStatus(String(localized: "Gateway not connected"), phase: .idle) let shouldResume = self.isEnabled @@ -1147,10 +1167,13 @@ final class TalkModeManager: NSObject { status: "queued") // Finishing owns shared chat/TTS state after microphone capture ends. // Publish it first so a replacement cannot overlap old finalizer cleanup. + guard let gatewayContext = activePushToTalk.gatewayContext else { + preconditionFailure("Agent push-to-talk requires a gateway context") + } self.startFinishingPushToTalk( captureId: captureId, transcript: transcript, - gatewayContext: activePushToTalk.gatewayContext) + gatewayContext: gatewayContext) // Reply generation can open interruption recognition. Keep the PTT // external-audio lease until that finalizer exits or is canceled. self.transferActivePushToTalkToFinalizer(captureId) @@ -1160,23 +1183,25 @@ final class TalkModeManager: NSObject { func beginPushToTalkOnce( maxDurationSeconds: TimeInterval = 12, + transcriptionOnly: Bool = false, canStartCapture: @MainActor () -> Bool = { true }, onCaptureReserved: @MainActor (String) -> Void = { _ in }) async throws -> TalkPushToTalkOnceStart { - if let captureId = self.activePTTCaptureId ?? self.finishingPushToTalk?.captureId { + if let captureId = activePTTCaptureId ?? finishingPushToTalk?.captureId { return .busy(OpenClawTalkPTTStopPayload( captureId: captureId, transcript: nil, status: "busy")) } - let start = try await self.beginPushToTalk( + let start = try await beginPushToTalk( + transcriptionOnly: transcriptionOnly, canStartCapture: canStartCapture, onCaptureReserved: onCaptureReserved) let captureId = start.captureId do { #if DEBUG - if let testPTTOnceStartedHandler = self.testPTTOnceStartedHandler { + if let testPTTOnceStartedHandler { await testPTTOnceStartedHandler() } #endif @@ -1199,7 +1224,7 @@ final class TalkModeManager: NSObject { case let .busy(payload): return payload case let .started(captureId): - guard let operation = self.pttOnceOperations[captureId] else { + guard let operation = pttOnceOperations[captureId] else { return OpenClawTalkPTTStopPayload(captureId: captureId, transcript: nil, status: "idle") } let payload = await withTaskCancellationHandler { @@ -1219,6 +1244,18 @@ final class TalkModeManager: NSObject { return self.cancelPushToTalk(captureId: captureId) } + func cancelPushToTalk(expectedTranscriptionOnly: Bool) -> OpenClawTalkPTTStopPayload { + guard let activePushToTalk, + activePushToTalk.transcriptionOnly == expectedTranscriptionOnly + else { + return OpenClawTalkPTTStopPayload( + captureId: UUID().uuidString, + transcript: nil, + status: "idle") + } + return self.cancelPushToTalk(captureId: activePushToTalk.captureId) + } + func cancelPushToTalk(captureId: String) -> OpenClawTalkPTTStopPayload { guard self.activePTTCaptureId == captureId else { return OpenClawTalkPTTStopPayload(captureId: captureId, transcript: nil, status: "idle") @@ -1257,6 +1294,40 @@ final class TalkModeManager: NSObject { } } + private func requestPushToTalkPermissions( + captureId: String, + canStartCapture: @MainActor () -> Bool) async throws + { + guard !self.allowSimulatorCapture else { return } + + let micOk = await Self.requestMicrophonePermission() + try self.ensurePushToTalkStartCurrent(captureId: captureId, canStartCapture: canStartCapture) + guard micOk else { + self.setStatus( + String(localized: "Microphone permission denied"), + phase: .idle, + watchPresentation: .localized("Microphone permission denied")) + throw NSError(domain: "TalkMode", code: 4, userInfo: [ + NSLocalizedDescriptionKey: "Microphone permission denied", + ]) + } + + let speechOk = await Self.requestSpeechPermission() + try self.ensurePushToTalkStartCurrent(captureId: captureId, canStartCapture: canStartCapture) + guard speechOk else { + let status = Self.permissionMessage( + kind: String(localized: "Speech recognition"), + status: SFSpeechRecognizer.authorizationStatus()) + self.setStatus( + status, + phase: .idle, + watchPresentation: .verbatim(status)) + throw NSError(domain: "TalkMode", code: 5, userInfo: [ + NSLocalizedDescriptionKey: "Speech recognition permission denied", + ]) + } + } + private static func pushToTalkStartCancelledError() -> NSError { NSError(domain: "TalkMode", code: 9, userInfo: [ NSLocalizedDescriptionKey: "PTT_CANCELLED: push-to-talk start was cancelled", @@ -1269,6 +1340,12 @@ final class TalkModeManager: NSObject { ]) } + private static func pushToTalkModeConflictError() -> NSError { + NSError(domain: "TalkMode", code: 10, userInfo: [ + NSLocalizedDescriptionKey: "PTT_BUSY: another capture mode owns the microphone", + ]) + } + private func pushToTalkOfflineError() -> NSError { self.setStatus(String(localized: "Offline"), phase: .idle) return NSError(domain: "TalkMode", code: 7, userInfo: [ @@ -1288,7 +1365,7 @@ final class TalkModeManager: NSObject { private func finishActivePushToTalk(_ captureId: String) { guard self.clearActivePushToTalk(captureId) else { return } - self.deactivateStandaloneAudioSessionIfIdle() + deactivateStandaloneAudioSessionIfIdle() self.pttAudioOwnershipEndHandler?(captureId) } @@ -1359,12 +1436,12 @@ final class TalkModeManager: NSObject { } private func clearFinishingPushToTalk(captureId: String, generation: UInt64) { - guard let finishing = self.finishingPushToTalk, + guard let finishing = finishingPushToTalk, finishing.captureId == captureId, finishing.generation == generation else { return } self.finishingPushToTalk = nil - self.deactivateStandaloneAudioSessionIfIdle() + deactivateStandaloneAudioSessionIfIdle() self.pttAudioOwnershipEndHandler?(captureId) if self.phase.isFinalizerTransient { self.setStatus( @@ -1478,7 +1555,7 @@ final class TalkModeManager: NSObject { self.micLevel = (self.micLevel * 0.80) + (raw * 0.20) self.updateNoiseFloorIfNeeded(raw) - let threshold: Double = if let floor = self.noiseFloor, self.noiseFloorReady { + let threshold: Double = if let floor = noiseFloor, noiseFloorReady { min(0.35, max(0.12, floor + 0.10)) } else { 0.18 @@ -1512,7 +1589,7 @@ final class TalkModeManager: NSObject { recognitionGeneration: UInt64) { guard self.recognitionGeneration == recognitionGeneration else { return } - if let pttCaptureId, self.activePTTCaptureId != pttCaptureId { + if let pttCaptureId, activePTTCaptureId != pttCaptureId { return } if let error, !self.handleRecognitionError(error) { @@ -1593,18 +1670,20 @@ final class TalkModeManager: NSObject { private func restartRecognitionAfterError(expectedGeneration: UInt64) async { guard self.canRestartNativeRecognition(expectedGeneration: expectedGeneration) else { return } // Avoid thrashing the audio engine if it’s already running. - if self.recognitionTask != nil, self.audioEngine.isRunning { return } + if self.recognitionTask != nil, self.audioEngine.isRunning { + return + } try? await Task.sleep(nanoseconds: 250_000_000) guard self.canRestartNativeRecognition(expectedGeneration: expectedGeneration) else { return } do { - try self.configureOwnedAudioSession() + try configureOwnedAudioSession() try self.startRecognition() self.isListening = true self.restoreListeningStatusAfterSpeechErrorRestart() GatewayDiagnostics.log("talk speech: recognition restarted") } catch { self.stopNativeCaptureAndDiscardTranscript() - self.deactivateAudioSession() + deactivateAudioSession() let msg = error.localizedDescription GatewayDiagnostics.log("talk speech: restart failed error=\(msg)") } @@ -1677,7 +1756,7 @@ final class TalkModeManager: NSObject { recognitionGeneration: UInt64) async { guard self.recognitionGeneration == recognitionGeneration else { return } - if let pttCaptureId, self.activePTTCaptureId != pttCaptureId { + if let pttCaptureId, activePTTCaptureId != pttCaptureId { return } let trimmed = transcript.trimmingCharacters(in: .whitespacesAndNewlines) @@ -1735,7 +1814,9 @@ final class TalkModeManager: NSObject { guard !transcript.isEmpty else { return } let lastActivity = [lastHeard, lastAudioActivity].compactMap(\.self).max() guard let lastActivity else { return } - if Date().timeIntervalSince(lastActivity) < self.silenceWindow { return } + if Date().timeIntervalSince(lastActivity) < self.silenceWindow { + return + } await self.processTranscript(transcript, restartAfter: true) return } @@ -1749,7 +1830,9 @@ final class TalkModeManager: NSObject { guard !transcript.isEmpty else { return } let lastActivity = [lastHeard, lastAudioActivity].compactMap(\.self).max() guard let lastActivity else { return } - if Date().timeIntervalSince(lastActivity) < self.silenceWindow { return } + if Date().timeIntervalSince(lastActivity) < self.silenceWindow { + return + } if let pttCaptureId { _ = self.endPushToTalk(captureId: pttCaptureId) } else { @@ -1794,16 +1877,16 @@ final class TalkModeManager: NSObject { self.continuousTranscriptProcessingGeneration = nil } } - guard let gateway = self.gateway else { + guard let gateway else { self.setStatus(String(localized: "Gateway not connected"), phase: .idle) self.scheduleContinuousResume(restartAfter) return } let sessionKey = self.mainSessionKey guard let gatewayRoute = await gateway.currentRoute(), - self.isCurrentTranscriptProcessing(generation), + isCurrentTranscriptProcessing(generation), self.gateway === gateway, - self.mainSessionKey == sessionKey + mainSessionKey == sessionKey else { return } await self.processTranscript( transcript, @@ -1823,7 +1906,7 @@ final class TalkModeManager: NSObject { transcriptProcessingGeneration generation: UInt64) async { let streamingOwner = TranscriptStreamingOwner() - await self.runTranscriptProcessing( + await runTranscriptProcessing( transcript, restartAfter: restartAfter, gateway: gateway, @@ -1932,7 +2015,7 @@ final class TalkModeManager: NSObject { "talk: chat.send terminal ack runId=\(runId) status=\(normalizedStatus)") return } - guard let completedSuccessfully = try await self.completeTranscriptResponse( + guard let completedSuccessfully = try await completeTranscriptResponse( acknowledgement: acknowledgement, startedAt: startedAt, gateway: gateway, @@ -2213,7 +2296,7 @@ final class TalkModeManager: NSObject { }) self.realtimeRelaySession = relaySession do { - try self.configureOwnedRealtimeAudioSession() + try configureOwnedRealtimeAudioSession() try await relaySession.start() guard self.realtimeRelaySession === relaySession, self.realtimeRelayGeneration == relayGeneration, @@ -2538,7 +2621,9 @@ final class TalkModeManager: NSObject { guard let content = msg["content"] as? [[String: Any]] else { continue } let text = content.compactMap { $0["text"] as? String }.joined(separator: "\n") let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines) - if !trimmed.isEmpty { return trimmed } + if !trimmed.isEmpty { + return trimmed + } } return nil } @@ -2658,7 +2743,7 @@ final class TalkModeManager: NSObject { self.startSpeechInterruptionRecognitionIfNeeded() self.setStatus(String(localized: "Speaking…"), phase: .speaking) - let result = await self.playElevenLabsStream( + let result = await playElevenLabsStream( rawStream, sampleRate: TalkTTSValidation.pcmSampleRate(from: outputFormat)) { mp3Format in @@ -2715,7 +2800,7 @@ final class TalkModeManager: NSObject { let synthesizer: any TalkGatewaySpeechSynthesizing if let gatewaySpeechSynthesizerOverride { synthesizer = gatewaySpeechSynthesizerOverride - } else if let gateway = gatewayOverride ?? self.gateway, let gatewayRoute { + } else if let gateway = gatewayOverride ?? gateway, let gatewayRoute { synthesizer = TalkGatewaySpeechClient(gateway: gateway, route: gatewayRoute) } else { throw NSError(domain: "TalkGatewaySpeech", code: 1, userInfo: [ @@ -2737,7 +2822,7 @@ final class TalkModeManager: NSObject { modelId: modelId, outputFormat: outputFormat, directive: directive)) - if let gatewayRoute, let gateway = gatewayOverride ?? self.gateway { + if let gatewayRoute, let gateway = gatewayOverride ?? gateway { guard await gateway.currentRoute() == gatewayRoute else { throw CancellationError() } } guard generation == self.speechGeneration, self.isSpeaking else { return } @@ -3411,7 +3496,7 @@ final class TalkModeManager: NSObject { client.streamSynthesize(voiceId: voiceId, request: request) } let playbackFormat = prefetchedAudio?.outputFormat ?? context.outputFormat - let result = await self.playElevenLabsStream( + let result = await playElevenLabsStream( rawStream, sampleRate: TalkTTSValidation.pcmSampleRate(from: playbackFormat)) { mp3Format in @@ -3595,7 +3680,9 @@ extension TalkModeManager { let trimmed = (value ?? "").trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { return nil } let normalized = trimmed.lowercased() - if let mapped = voiceAliases[normalized] { return mapped } + if let mapped = voiceAliases[normalized] { + return mapped + } if self.voiceAliases.values.contains(where: { $0.caseInsensitiveCompare(trimmed) == .orderedSame }) { return trimmed } @@ -3614,10 +3701,14 @@ extension TalkModeManager { if Self.isLikelyVoiceId(trimmed) { return trimmed } - if let resolved = resolveVoiceAlias(trimmed) { return resolved } + if let resolved = resolveVoiceAlias(trimmed) { + return resolved + } self.logger.warning("unknown voice alias \(trimmed, privacy: .public)") } - if let fallbackVoiceId { return fallbackVoiceId } + if let fallbackVoiceId { + return fallbackVoiceId + } do { let voices = try await ElevenLabsTTSClient(apiKey: apiKey).listVoices() @@ -3653,7 +3744,9 @@ extension TalkModeManager { guard !trimmed.isEmpty else { return nil } guard trimmed != Self.redactedConfigSentinel else { return nil } // Config values may be env placeholders (for example `${ELEVENLABS_API_KEY}`). - if trimmed.hasPrefix("${"), trimmed.hasSuffix("}") { return nil } + if trimmed.hasPrefix("${"), trimmed.hasSuffix("}") { + return nil + } return trimmed } @@ -3882,7 +3975,7 @@ extension TalkModeManager { } private func ensureTalkConfigLoadedForStart() async { - if self.gatewayTalkConfigLoaded || self.gatewayTalkPermissionState.isApprovalRequestInProgress { + if self.gatewayTalkConfigLoaded { GatewayDiagnostics.log( "talk.timeline config cached permission=\(self.gatewayTalkPermissionState.statusLabel) " + "loadedAt=\(self.talkConfigLoadedAt?.timeIntervalSince1970 ?? 0)") @@ -3901,7 +3994,7 @@ extension TalkModeManager { gatewayRoute: GatewayNodeSessionRoute? = nil, shouldApply: @MainActor @Sendable () -> Bool = { true }) async { - guard let gateway = gatewayOverride ?? self.gateway else { return } + guard let gateway = gatewayOverride ?? gateway else { return } do { guard let loaded = try await loadTalkConfig(from: gateway, gatewayRoute: gatewayRoute) else { return } if let gatewayRoute { @@ -4172,8 +4265,8 @@ extension TalkModeManager { self.gatewayTalkApiKeyConfigured = false } - func markTalkPermissionUpgradeRequested(requestId: String?) { - self.gatewayTalkPermissionState = .upgradeRequested(requestId: requestId) + func markTalkPermissionUpgradeRequested() { + self.gatewayTalkPermissionState = .upgradeRequested self.setStatus(String(localized: "Approval requested"), phase: .idle) } @@ -4331,9 +4424,13 @@ private final class AudioTapDiagnostics: @unchecked Sendable { let resolvedRms = Float(TalkAudioLevel.rms(buffer: buffer)) self.lock.lock() - if resolvedRms > self.maxRmsWindow { self.maxRmsWindow = resolvedRms } + if resolvedRms > self.maxRmsWindow { + self.maxRmsWindow = resolvedRms + } let maxRms = self.maxRmsWindow - if shouldLog { self.maxRmsWindow = 0 } + if shouldLog { + self.maxRmsWindow = 0 + } self.lock.unlock() if shouldEmitLevel, let onLevel { diff --git a/apps/ios/Sources/Voice/TalkPermissionPromptView.swift b/apps/ios/Sources/Voice/TalkPermissionPromptView.swift deleted file mode 100644 index 74a98f5b7177..000000000000 --- a/apps/ios/Sources/Voice/TalkPermissionPromptView.swift +++ /dev/null @@ -1,178 +0,0 @@ -import SwiftUI - -struct TalkPermissionPromptView: View { - enum Style { - case card - case sheet - } - - @Environment(NodeAppModel.self) private var appModel - - let style: Style - var onPermissionReady: (() -> Void)? - - private var state: TalkGatewayPermissionState { - self.appModel.talkMode.gatewayTalkPermissionState - } - - private var requestIsPending: Bool { - self.state.isApprovalRequestInProgress - } - - private var pollTaskKey: String { - switch self.state { - case .requestingUpgrade: - "requesting" - case let .upgradeRequested(requestId): - "pending:\(requestId ?? "")" - default: - "idle:\(self.state.statusLabel)" - } - } - - var body: some View { - VStack(alignment: .leading, spacing: self.style == .sheet ? 16 : 12) { - HStack(alignment: .top, spacing: 12) { - Image(systemName: self.iconSystemName) - .font(OpenClawType.title3) - .foregroundStyle(self.requestIsPending ? OpenClawBrand.warn : OpenClawBrand.accent) - .frame(width: 28, height: 28) - - VStack(alignment: .leading, spacing: 6) { - Text(self.titleText) - .font(self.style == .sheet ? OpenClawType.title3SemiBold : OpenClawType.headline) - Text(self.messageText) - .font(OpenClawType.footnote) - .foregroundStyle(.secondary) - .fixedSize(horizontal: false, vertical: true) - } - } - - if let failureMessage = self.state.failureMessage { - Label(failureMessage, systemImage: "exclamationmark.triangle.fill") - .font(OpenClawType.footnote) - .foregroundStyle(OpenClawBrand.danger) - .fixedSize(horizontal: false, vertical: true) - } - - if let requestId = self.state.requestId { - LabeledContent { - Text(requestId) - .font(OpenClawType.monoSmall) - .foregroundStyle(.secondary) - .textSelection(.enabled) - } label: { - Text("Request ID") - .font(OpenClawType.footnoteSemiBold) - } - } - - HStack(spacing: 10) { - Button { - self.appModel.requestTalkPermissionUpgrade() - } label: { - if case .requestingUpgrade = self.state { - Label { - Text("Sending...") - .font(OpenClawType.subheadSemiBold) - } icon: { - ProgressView() - } - } else { - Label(self.primaryButtonTitle, systemImage: self.primaryButtonSystemImage) - .font(OpenClawType.subheadSemiBold) - } - } - .buttonStyle(.borderedProminent) - .disabled(self.state == .requestingUpgrade) - - Button { - Task { await self.appModel.talkMode.reloadConfig() } - } label: { - Label("Retry", systemImage: "arrow.triangle.2.circlepath") - .font(OpenClawType.subheadSemiBold) - } - .buttonStyle(.bordered) - } - } - .padding(self.style == .card || self.style == .sheet ? 16 : 0) - .background { - if self.style == .card || self.style == .sheet { - RoundedRectangle(cornerRadius: OpenClawRadius.md, style: .continuous) - .fill(.thinMaterial) - } - } - .overlay { - if self.style == .card || self.style == .sheet { - RoundedRectangle(cornerRadius: OpenClawRadius.md, style: .continuous) - .stroke(OpenClawBrand.accent.opacity(0.20), lineWidth: 1) - } - } - .task(id: self.pollTaskKey) { - guard self.requestIsPending else { return } - await self.pollUntilReady() - } - .onChange(of: self.state) { _, newState in - if newState == .ready { - self.onPermissionReady?() - } - } - } - - private var iconSystemName: String { - switch self.state { - case .requestingUpgrade: - "paperplane.fill" - case .upgradeRequested: - "hourglass" - case .requestFailed: - "exclamationmark.triangle.fill" - default: - "key.fill" - } - } - - private var titleText: String { - switch self.state { - case .requestingUpgrade: - "Sending approval request" - case .upgradeRequested: - "Approval sent" - case .requestFailed: - "Could not request approval" - default: - "Enable Talk" - } - } - - private var messageText: String { - switch self.state { - case .requestingUpgrade: - "Sending a new pairing request to your gateway..." - case .upgradeRequested: - "Approve this request on your gateway. Talk will start automatically when approval lands." - default: - "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from " + - "this device to the voice provider." - } - } - - private var primaryButtonTitle: String { - self.requestIsPending ? "Request Again" : "Send Approval Request" - } - - private var primaryButtonSystemImage: String { - self.requestIsPending ? "arrow.clockwise" : "paperplane.fill" - } - - private func pollUntilReady() async { - while !Task.isCancelled { - try? await Task.sleep(nanoseconds: 3_000_000_000) - if Task.isCancelled { return } - await self.appModel.pollTalkPermissionUpgrade() - if !self.appModel.talkMode.gatewayTalkPermissionState.requiresTalkPermissionAction { - return - } - } - } -} diff --git a/apps/ios/Tests/GatewayConnectionControllerTests.swift b/apps/ios/Tests/GatewayConnectionControllerTests.swift index 4eec9f46f2a3..4f005e7b0a81 100644 --- a/apps/ios/Tests/GatewayConnectionControllerTests.swift +++ b/apps/ios/Tests/GatewayConnectionControllerTests.swift @@ -347,6 +347,18 @@ private func waitForActiveGateway(stableID: String, appModel: NodeAppModel) asyn #expect(options.scopes.contains("operator.talk.secrets")) } + @Test func `permission upgrade polling preserves active connection attempts`() { + #expect(NodeAppModel.shouldRestartTalkPermissionUpgradePoll( + hasOperatorGatewayTask: false, + hasReconnectTask: false)) + #expect(!NodeAppModel.shouldRestartTalkPermissionUpgradePoll( + hasOperatorGatewayTask: true, + hasReconnectTask: false)) + #expect(!NodeAppModel.shouldRestartTalkPermissionUpgradePoll( + hasOperatorGatewayTask: false, + hasReconnectTask: true)) + } + @Test func `operator admin scope requests only when shared auth or already granted`() { #expect( !NodeAppModel._test_shouldRequestOperatorAdminScope( diff --git a/apps/ios/Tests/GatewayStatusBuilderTests.swift b/apps/ios/Tests/GatewayStatusBuilderTests.swift index 6d5020448304..62de4acb8f84 100644 --- a/apps/ios/Tests/GatewayStatusBuilderTests.swift +++ b/apps/ios/Tests/GatewayStatusBuilderTests.swift @@ -34,12 +34,43 @@ struct GatewayStatusBuilderTests { #expect(state == .error) } - @Test func `chat gateway pill labels match display state`() { - #expect(ChatProTab.gatewayPillTitle(state: .disconnected, isGatewayUsable: false) == "Offline") - #expect(ChatProTab.gatewayPillTitle(state: .connecting, isGatewayUsable: false) == "Connecting") - #expect(ChatProTab.gatewayPillTitle(state: .error, isGatewayUsable: false) == "Attention") - #expect(ChatProTab.gatewayPillTitle(state: .connected, isGatewayUsable: true) == "Connected") - #expect(ChatProTab.gatewayPillTitle(state: .connected, isGatewayUsable: false) == "Unavailable") + @Test func `chat gateway status labels match display state`() { + #expect(ChatProTab.gatewayStatusTitle(state: .disconnected, isGatewayUsable: false) == "Offline") + #expect(ChatProTab.gatewayStatusTitle(state: .connecting, isGatewayUsable: false) == "Connecting") + #expect(ChatProTab.gatewayStatusTitle(state: .error, isGatewayUsable: false) == "Attention") + #expect(ChatProTab.gatewayStatusTitle(state: .connected, isGatewayUsable: true) == "Connected") + #expect(ChatProTab.gatewayStatusTitle(state: .connected, isGatewayUsable: false) == "Unavailable") + } + + @Test func `chat gateway status tones separate healthy issue and offline states`() { + #expect(ChatProTab.gatewayStatusTone(state: .connected, isGatewayUsable: true) == .success) + #expect(ChatProTab.gatewayStatusTone(state: .connected, isGatewayUsable: false) == .warning) + #expect(ChatProTab.gatewayStatusTone(state: .connecting, isGatewayUsable: false) == .warning) + #expect(ChatProTab.gatewayStatusTone(state: .error, isGatewayUsable: false) == .warning) + #expect(ChatProTab.gatewayStatusTone(state: .disconnected, isGatewayUsable: false) == .error) + } + + @Test func `chat gateway status expands on tap or whenever gateway needs attention`() { + #expect(!ChatProTab.gatewayStatusShouldExpand( + state: .connected, + isGatewayUsable: true, + isManuallyExpanded: false)) + #expect(ChatProTab.gatewayStatusShouldExpand( + state: .connected, + isGatewayUsable: true, + isManuallyExpanded: true)) + #expect(ChatProTab.gatewayStatusShouldExpand( + state: .connecting, + isGatewayUsable: false, + isManuallyExpanded: false)) + #expect(ChatProTab.gatewayStatusShouldExpand( + state: .error, + isGatewayUsable: false, + isManuallyExpanded: false)) + #expect(ChatProTab.gatewayStatusShouldExpand( + state: .disconnected, + isGatewayUsable: false, + isManuallyExpanded: false)) } @Test func `chat agent badge rejects placeholder question mark`() { diff --git a/apps/ios/Tests/LiveActivityPresentationArbiterTests.swift b/apps/ios/Tests/LiveActivityPresentationArbiterTests.swift new file mode 100644 index 000000000000..a7a6d9f9f291 --- /dev/null +++ b/apps/ios/Tests/LiveActivityPresentationArbiterTests.swift @@ -0,0 +1,229 @@ +import Foundation +import Testing +@testable import OpenClaw + +struct LiveActivityPresentationArbiterTests { + @Test func `voice sample buffer quantizes clamps and stays bounded`() { + var buffer = LiveActivityVoiceSampleBuffer(capacity: 3) + + for level in [-1.0, 0.25, 0.5, 2.0] { + if let sample = LiveActivityVoiceSampleBuffer.quantize(level) { + buffer.append(sample) + } + } + + #expect(buffer.payload == [64, 128, 255]) + #expect(LiveActivityVoiceSampleBuffer.quantize(.infinity) == nil) + buffer.reset() + #expect(buffer.payload == nil) + } + + @Test + func `attention outranks tools voice and connection`() { + let now = Date() + var arbiter = LiveActivityPresentationArbiter() + arbiter.setConnection(Self.request(status: .connecting, detail: nil, startedAt: now)) + arbiter.setVoice(Self.request(status: .voiceListening, detail: nil, startedAt: now)) + arbiter.startTool( + id: "tool-1", + request: Self.request(status: .toolRunning, detail: "first", startedAt: now)) + arbiter.setAttention(Self.request(status: .approvalNeeded, detail: nil, startedAt: now)) + + #expect(arbiter.current?.state.status == .approvalNeeded) + + arbiter.setAttention(nil) + #expect(arbiter.current?.state.status == .toolRunning) + } + + @Test + func `parallel tools resume the newest remaining tool then voice`() { + let now = Date() + var arbiter = LiveActivityPresentationArbiter() + arbiter.setVoice(Self.request(status: .voiceActive, detail: nil, startedAt: now)) + arbiter.startTool( + id: "tool-1", + request: Self.request(status: .toolRunning, detail: "first", startedAt: now)) + arbiter.startTool( + id: "tool-2", + request: Self.request(status: .toolRunning, detail: "second", startedAt: now)) + + #expect(arbiter.activeToolCount == 2) + #expect(arbiter.current?.state.verbatimDetail == "second") + + arbiter.endTool(id: "tool-2", sessionKey: "main") + #expect(arbiter.current?.state.verbatimDetail == "first") + + arbiter.endTool(id: "tool-1", sessionKey: "main") + #expect(arbiter.current?.state.status == .voiceActive) + } + + @Test + func `tool identities are scoped to their owning session`() { + let now = Date() + var arbiter = LiveActivityPresentationArbiter() + arbiter.startTool( + id: "shared-id", + request: Self.request( + status: .toolRunning, + detail: "main", + startedAt: now, + sessionKey: "main")) + arbiter.startTool( + id: "shared-id", + request: Self.request( + status: .toolRunning, + detail: "other", + startedAt: now, + sessionKey: "other")) + + #expect(arbiter.activeToolCount == 2) + arbiter.endTool(id: "shared-id", sessionKey: "main") + #expect(arbiter.activeToolCount == 1) + #expect(arbiter.current?.sessionKey == "other") + } + + @Test + func `presentation lifecycle only carries across the same owner`() { + let startedAt = Date(timeIntervalSince1970: 100) + let now = Date(timeIntervalSince1970: 500) + let existing = Self.request( + status: .connecting, + detail: nil, + startedAt: startedAt, + sessionKey: "main") + + #expect(LiveActivityManager.lifecycleStartedAt( + existing: existing, + agentName: "Aiden", + sessionKey: "main", + now: now) == startedAt) + #expect(LiveActivityManager.lifecycleStartedAt( + existing: existing, + agentName: "Aiden", + sessionKey: "other", + now: now) == now) + #expect(LiveActivityManager.lifecycleStartedAt( + existing: existing, + agentName: "Luna", + sessionKey: "main", + now: now) == now) + } + + @Test + func `voice status distinguishes active listening and speaking`() { + #expect(LiveActivityPresentationArbiter.voiceStatus(isListening: false, isSpeaking: false) == .voiceActive) + #expect(LiveActivityPresentationArbiter.voiceStatus(isListening: true, isSpeaking: false) == .voiceListening) + #expect(LiveActivityPresentationArbiter.voiceStatus(isListening: true, isSpeaking: true) == .voiceSpeaking) + } + + @Test + func `voice sample history survives active phase changes`() { + #expect(LiveActivityManager.shouldResetVoiceSamples(previousStatus: nil)) + #expect(!LiveActivityManager.shouldResetVoiceSamples(previousStatus: .voiceActive)) + #expect(!LiveActivityManager.shouldResetVoiceSamples(previousStatus: .voiceListening)) + #expect(!LiveActivityManager.shouldResetVoiceSamples(previousStatus: .voiceSpeaking)) + } + + @Test + func `reconnect clears connection state without hiding active voice`() { + let now = Date() + var arbiter = LiveActivityPresentationArbiter() + arbiter.setConnection(Self.request(status: .reconnecting, detail: nil, startedAt: now)) + arbiter.setAttention(Self.request(status: .actionRequired, detail: nil, startedAt: now)) + arbiter.setVoice(Self.request(status: .voiceListening, detail: nil, startedAt: now)) + + arbiter.clearConnectionState() + + #expect(arbiter.attention == nil) + #expect(arbiter.connection == nil) + #expect(arbiter.current?.state.status == .voiceListening) + } + + @Test + func `live voice permanently replaces a hydrated tool fallback`() { + let now = Date() + var arbiter = LiveActivityPresentationArbiter() + arbiter.adoptInitialHydratedToolFallback( + Self.request(status: .toolRunning, detail: "restored", startedAt: now)) + + arbiter.setVoice(Self.request(status: .voiceListening, detail: nil, startedAt: now)) + #expect(arbiter.hydratedToolFallback == nil) + #expect(arbiter.current?.state.status == .voiceListening) + + arbiter.setVoice(nil) + #expect(arbiter.current == nil) + } + + @Test + func `hydration trusts renewed voice and tool stale dates`() { + let now = Date() + let originalStart = now.addingTimeInterval(-600) + let renewedStaleDate = now.addingTimeInterval(240) + + #expect(LiveActivityManager.shouldHydrate( + status: .voiceSpeaking, + startedAt: originalStart, + staleDate: renewedStaleDate, + now: now)) + #expect(LiveActivityManager.shouldHydrate( + status: .toolRunning, + startedAt: originalStart, + staleDate: renewedStaleDate, + now: now)) + } + + @Test + func `hydration rejects expired and old non-heartbeat presentations`() { + let now = Date() + let originalStart = now.addingTimeInterval(-600) + + #expect(!LiveActivityManager.shouldHydrate( + status: .voiceListening, + startedAt: originalStart, + staleDate: now.addingTimeInterval(-1), + now: now)) + #expect(!LiveActivityManager.shouldHydrate( + status: .reconnecting, + startedAt: originalStart, + staleDate: now.addingTimeInterval(240), + now: now)) + } + + @Test + func `voice live activity lifecycle is app owned`() throws { + let testsDirectory = URL(fileURLWithPath: #filePath).deletingLastPathComponent() + let sourcesDirectory = testsDirectory.deletingLastPathComponent().appendingPathComponent("Sources") + let appSource = try String( + contentsOf: sourcesDirectory.appendingPathComponent("OpenClawApp.swift"), + encoding: .utf8) + let chatSource = try String( + contentsOf: sourcesDirectory.appendingPathComponent("Design/ChatProTab.swift"), + encoding: .utf8) + let coordinatorSource = try String( + contentsOf: sourcesDirectory.appendingPathComponent( + "LiveActivity/VoiceLiveActivityCoordinator.swift"), + encoding: .utf8) + + #expect(appSource.contains("self.voiceLiveActivityCoordinator.start(appModel: self.appModel)")) + #expect(coordinatorSource.contains("LiveActivityManager.shared.showVoice(")) + #expect(coordinatorSource.contains("LiveActivityManager.shared.updateVoiceLevel(")) + #expect(!chatSource.contains("syncVoiceLiveActivity")) + #expect(!chatSource.contains("LiveActivityManager.shared.showVoice(")) + } + + private static func request( + status: OpenClawActivityAttributes.ContentState.Status, + detail: String?, + startedAt: Date, + sessionKey: String = "main") -> LiveActivityPresentationRequest + { + LiveActivityPresentationRequest( + state: OpenClawActivityAttributes.ContentState( + status: status, + verbatimDetail: detail, + startedAt: startedAt), + staleDate: startedAt.addingTimeInterval(300), + agentName: "Aiden", + sessionKey: sessionKey) + } +} diff --git a/apps/ios/Tests/NodeAppModelInvokeTests.swift b/apps/ios/Tests/NodeAppModelInvokeTests.swift index 8ef3e79add3f..87b95f07e3ff 100644 --- a/apps/ios/Tests/NodeAppModelInvokeTests.swift +++ b/apps/ios/Tests/NodeAppModelInvokeTests.swift @@ -2844,6 +2844,50 @@ private func overrideNotificationServingPreference(_ enabled: Bool) -> () -> Voi #expect(!talkMode.isListening) } + @Test @MainActor func `enabling unified voice requests a missing Talk scope upgrade`() async throws { + let talkMode = TalkModeManager(allowSimulatorCapture: true) + let appModel = NodeAppModel(talkMode: talkMode) + let config = try GatewayConnectConfig( + url: #require(URL(string: "wss://127.0.0.1:1")), + stableID: "manual|gateway.example.com|443", + tls: nil, + token: nil, + bootstrapToken: nil, + password: nil, + nodeOptions: GatewayConnectOptions( + role: "node", + scopes: [], + caps: [], + commands: [], + permissions: [:], + clientId: "openclaw-ios", + clientMode: "node", + clientDisplayName: nil)) + appModel._test_setActiveGatewayConnectConfig(config) + talkMode.gatewayTalkPermissionState = .missingScope("operator.talk.secrets") + defer { + appModel.setTalkEnabled(false) + appModel.disconnectGateway() + } + + appModel.setTalkEnabled(true) + await waitForTalkCondition { talkMode.gatewayTalkPermissionState == .requestingUpgrade } + + #expect(appModel._test_forceTalkPermissionUpgradeRequest()) + appModel.gatewayAutoReconnectEnabled = false + appModel.gatewayPairingPaused = true + appModel.setTalkEnabled(false) + #expect(!appModel._test_forceTalkPermissionUpgradeRequest()) + #expect(appModel.gatewayAutoReconnectEnabled) + #expect(!appModel.gatewayPairingPaused) + + appModel.gatewayAutoReconnectEnabled = false + appModel.gatewayPairingPaused = true + appModel.setTalkEnabled(false) + #expect(!appModel.gatewayAutoReconnectEnabled) + #expect(appModel.gatewayPairingPaused) + } + @Test @MainActor func `stale PTT recognition callback cannot mutate a newer capture`() async throws { let talkMode = TalkModeManager(allowSimulatorCapture: true) talkMode.updateGatewayConnected(true) @@ -2867,6 +2911,257 @@ private func overrideNotificationServingPreference(_ enabled: Bool) -> () -> Voi _ = await talkMode.awaitPushToTalkOnce(second) } + @Test @MainActor func `chat dictation returns transcript and releases audio ownership`() async throws { + let talkMode = TalkModeManager(allowSimulatorCapture: true) + let appModel = NodeAppModel(talkMode: talkMode) + let transcription = Task { @MainActor in + try await appModel.transcribeChatDraft() + } + await waitForTalkCondition { appModel.isChatDictationActive } + let captureId = try #require(talkMode._test_activePushToTalkCaptureId()) + #expect(appModel._test_pttVoiceWakeLeaseCaptureIds() == [captureId]) + await talkMode._test_handlePushToTalkTranscript( + "draft from speech", + isFinal: false, + captureId: captureId) + + appModel.finishChatDictation() + let transcript = try await transcription.value + #expect(transcript == "draft from speech") + #expect(!appModel.isChatDictationActive) + #expect(appModel._test_pttVoiceWakeLeaseCaptureIds().isEmpty) + } + + @Test @MainActor func `cancelling chat dictation clears capture and voice wake lease`() async throws { + let talkMode = TalkModeManager(allowSimulatorCapture: true) + let appModel = NodeAppModel(talkMode: talkMode) + let transcription = Task { @MainActor in + try await appModel.transcribeChatDraft() + } + await waitForTalkCondition { appModel.isChatDictationActive } + let captureId = try #require(talkMode._test_activePushToTalkCaptureId()) + await talkMode._test_handlePushToTalkTranscript( + "discard this partial draft", + isFinal: false, + captureId: captureId) + + appModel.cancelChatDictation() + + let transcript = try await transcription.value + #expect(transcript == nil) + #expect(!appModel.isChatDictationActive) + #expect(talkMode._test_activePushToTalkCaptureId() == nil) + #expect(appModel._test_pttVoiceWakeLeaseCaptureIds().isEmpty) + } + + @Test @MainActor func `remote PTT cannot adopt or interrupt chat dictation`() async throws { + let talkMode = TalkModeManager(allowSimulatorCapture: true) + let appModel = NodeAppModel(talkMode: talkMode) + talkMode.updateGatewayConnected(true) + let transcription = Task { @MainActor in + try await appModel.transcribeChatDraft() + } + await waitForTalkCondition { appModel.isChatDictationActive } + let captureId = try #require(talkMode._test_activePushToTalkCaptureId()) + + let remoteStart = await appModel._test_handleInvoke( + talkRequest(id: "remote-start-during-dictation", command: .pttStart)) + #expect(!remoteStart.ok) + #expect(remoteStart.error?.message.contains("PTT_BUSY") == true) + + for command in [OpenClawTalkCommand.pttStop, .pttCancel] { + let response = await appModel._test_handleInvoke( + talkRequest(id: "remote-\(command.rawValue)-during-dictation", command: command)) + let payload = try decodeTalkPayload(OpenClawTalkPTTStopPayload.self, from: response) + #expect(payload.status == "idle") + #expect(payload.captureId != captureId) + #expect(talkMode._test_activePushToTalkCaptureId() == captureId) + #expect(appModel.isChatDictationActive) + #expect(appModel._test_pttVoiceWakeLeaseCaptureIds() == [captureId]) + } + + await talkMode._test_handlePushToTalkTranscript( + "draft remains local", + isFinal: false, + captureId: captureId) + appModel.finishChatDictation() + + #expect(try await transcription.value == "draft remains local") + #expect(!appModel.isChatDictationActive) + #expect(appModel._test_pttVoiceWakeLeaseCaptureIds().isEmpty) + } + + @Test @MainActor func `chat dictation refuses a capture it did not reserve`() async throws { + let talkMode = TalkModeManager(allowSimulatorCapture: true) + let appModel = NodeAppModel(talkMode: talkMode) + let existing = try await talkMode.beginPushToTalkOnce( + maxDurationSeconds: 30, + transcriptionOnly: true) + let captureId = try #require(talkMode._test_activePushToTalkCaptureId()) + + let transcript = try await appModel.transcribeChatDraft() + + #expect(transcript == nil) + #expect(!appModel.isChatDictationActive) + #expect(talkMode._test_activePushToTalkCaptureId() == captureId) + _ = talkMode.cancelPushToTalk(captureId: captureId) + _ = await talkMode.awaitPushToTalkOnce(existing) + } + + @Test @MainActor func `gateway disconnect preserves local chat dictation`() async throws { + let talkMode = TalkModeManager(allowSimulatorCapture: true) + let appModel = NodeAppModel(talkMode: talkMode) + talkMode.updateGatewayConnected(true) + let transcription = Task { @MainActor in + try await appModel.transcribeChatDraft() + } + await waitForTalkCondition { appModel.isChatDictationActive } + let captureId = try #require(talkMode._test_activePushToTalkCaptureId()) + await talkMode._test_handlePushToTalkTranscript( + "draft survives disconnect", + isFinal: false, + captureId: captureId) + + talkMode.updateGatewayConnected(false) + + #expect(talkMode._test_activePushToTalkCaptureId() == captureId) + appModel.finishChatDictation() + #expect(try await transcription.value == "draft survives disconnect") + #expect(!appModel.isChatDictationActive) + #expect(appModel._test_pttVoiceWakeLeaseCaptureIds().isEmpty) + } + + @Test @MainActor func `gateway replacement preserves local chat dictation`() async throws { + let talkMode = TalkModeManager(allowSimulatorCapture: true) + let appModel = NodeAppModel(talkMode: talkMode) + let initialGateway = GatewayNodeSession() + let replacementGateway = GatewayNodeSession() + talkMode.attachGateway(initialGateway) + let transcription = Task { @MainActor in + try await appModel.transcribeChatDraft() + } + await waitForTalkCondition { appModel.isChatDictationActive } + let captureId = try #require(talkMode._test_activePushToTalkCaptureId()) + await talkMode._test_handlePushToTalkTranscript( + "draft survives replacement", + isFinal: false, + captureId: captureId) + + talkMode.attachGateway(replacementGateway) + + #expect(talkMode._test_activePushToTalkCaptureId() == captureId) + appModel.finishChatDictation() + #expect(try await transcription.value == "draft survives replacement") + #expect(!appModel.isChatDictationActive) + #expect(appModel._test_pttVoiceWakeLeaseCaptureIds().isEmpty) + } + + @Test @MainActor func `route and remote PTT invalidation preserve dictation preparation`() async throws { + let talkMode = TalkModeManager(allowSimulatorCapture: true) + let appModel = NodeAppModel(talkMode: talkMode) + let barrier = TalkPreparationBarrier() + appModel._test_setTalkCapturePreparationHandler { await barrier.suspendFirstPreparation() } + defer { + barrier.release() + appModel._test_setTalkCapturePreparationHandler(nil) + appModel.voiceWake.stop() + } + + let transcription = Task { @MainActor in + try await appModel.transcribeChatDraft() + } + await barrier.waitUntilEntered() + + #expect(await appModel._test_handleInvoke(talkRequest(id: "remote-cancel", command: .pttCancel)).ok) + appModel._test_invalidateOperatorTalkRoute() + barrier.release() + + await waitForTalkCondition { appModel.isChatDictationActive } + let captureId = try #require(talkMode._test_activePushToTalkCaptureId()) + await talkMode._test_handlePushToTalkTranscript( + "draft survives preparation invalidation", + isFinal: false, + captureId: captureId) + appModel.finishChatDictation() + + #expect(try await transcription.value == "draft survives preparation invalidation") + #expect(!appModel.isChatDictationActive) + #expect(appModel._test_pttVoiceWakeLeaseCaptureIds().isEmpty) + } + + @Test @MainActor func `backgrounding invalidates dictation preparation`() async { + let talkMode = TalkModeManager(allowSimulatorCapture: true) + let appModel = NodeAppModel(talkMode: talkMode) + let barrier = TalkPreparationBarrier() + appModel._test_setTalkCapturePreparationHandler { await barrier.suspendFirstPreparation() } + defer { + barrier.release() + appModel._test_setTalkCapturePreparationHandler(nil) + appModel.setScenePhase(.active) + appModel.voiceWake.stop() + } + + let transcription = Task { @MainActor in + try await appModel.transcribeChatDraft() + } + await barrier.waitUntilEntered() + + appModel.setScenePhase(.background) + barrier.release() + + await #expect(throws: Error.self) { + try await transcription.value + } + #expect(!appModel.isChatDictationActive) + #expect(talkMode._test_activePushToTalkCaptureId() == nil) + #expect(appModel._test_pttVoiceWakeLeaseCaptureIds().isEmpty) + } + + @Test @MainActor func `cancelling invalidates dictation preparation before capture reservation`() async { + let talkMode = TalkModeManager(allowSimulatorCapture: true) + let appModel = NodeAppModel(talkMode: talkMode) + let barrier = TalkPreparationBarrier() + appModel._test_setTalkCapturePreparationHandler { await barrier.suspendFirstPreparation() } + defer { + barrier.release() + appModel._test_setTalkCapturePreparationHandler(nil) + appModel.voiceWake.stop() + } + + let transcription = Task { @MainActor in + try await appModel.transcribeChatDraft() + } + await barrier.waitUntilEntered() + + appModel.cancelChatDictation() + barrier.release() + + await #expect(throws: Error.self) { + try await transcription.value + } + #expect(!appModel.isChatDictationActive) + #expect(talkMode._test_activePushToTalkCaptureId() == nil) + #expect(appModel._test_pttVoiceWakeLeaseCaptureIds().isEmpty) + } + + @Test @MainActor func `backgrounding cancels chat dictation and preserves audio admission`() async throws { + let talkMode = TalkModeManager(allowSimulatorCapture: true) + let appModel = NodeAppModel(talkMode: talkMode) + defer { appModel.setScenePhase(.active) } + let transcription = Task { @MainActor in + try await appModel.transcribeChatDraft() + } + await waitForTalkCondition { appModel.isChatDictationActive } + + appModel.setScenePhase(.background) + + let transcript = try await transcription.value + #expect(transcript == nil) + #expect(!appModel.isChatDictationActive) + #expect(talkMode._test_activePushToTalkCaptureId() == nil) + #expect(appModel._test_pttVoiceWakeLeaseCaptureIds().isEmpty) + } + @Test @MainActor func `stale continuous recognition callback cannot stop newer PTT`() async throws { let talkMode = TalkModeManager(allowSimulatorCapture: true) talkMode.updateGatewayConnected(true) diff --git a/apps/ios/Tests/RootTabsPresentationTests.swift b/apps/ios/Tests/RootTabsPresentationTests.swift index 2b639420b5cc..902d517b10c0 100644 --- a/apps/ios/Tests/RootTabsPresentationTests.swift +++ b/apps/ios/Tests/RootTabsPresentationTests.swift @@ -100,7 +100,7 @@ struct RootTabsPresentationTests { let destinationIDs = RootTabs.SidebarDestination.allCases.map(\.rawValue) #expect(groups.map(\.title) == ["CHAT", "CONTROL", "SETTINGS", "REFERENCE"]) - #expect(groups[0].destinations.map(\.rawValue) == ["chat", "talk"]) + #expect(groups[0].destinations.map(\.rawValue) == ["chat"]) #expect(groups[1].destinations == [ .overview, .activity, @@ -119,7 +119,6 @@ struct RootTabsPresentationTests { #expect(groups[3].destinations == [.docs]) #expect(destinationIDs == [ "chat", - "talk", "overview", "activity", "agents", @@ -157,15 +156,14 @@ struct RootTabsPresentationTests { #expect(RootTabs.SidebarDestination.gateway.sidebarTitle == "Connection") } - @Test func `phone hub uses root tabs only for native chat agent and gateway`() { + @Test func `phone hub routes only root destinations out of Control`() { #expect(RootTabs.shouldOpenRootTabFromPhoneHub(.chat)) - #expect(RootTabs.shouldOpenRootTabFromPhoneHub(.talk)) #expect(RootTabs.shouldOpenRootTabFromPhoneHub(.agents)) #expect(RootTabs.shouldOpenRootTabFromPhoneHub(.gateway)) #expect(RootTabs.shouldOpenRootTabFromPhoneHub(.settings)) for destination in RootTabs.SidebarDestination.allCases - where destination != .chat && destination != .talk && destination != .agents && destination != .gateway && + where destination != .chat && destination != .agents && destination != .gateway && destination != .settings { #expect(!RootTabs.shouldOpenRootTabFromPhoneHub(destination)) @@ -197,15 +195,15 @@ struct RootTabsPresentationTests { #expect(RootTabs.initialTab(arguments: ["OpenClaw", "--openclaw-initial-tab", "control"]) == .control) #expect(RootTabs.initialTab(arguments: ["OpenClaw", "--openclaw-initial-tab", "overview"]) == .control) #expect(RootTabs.initialTab(arguments: ["OpenClaw", "--openclaw-initial-tab", "chat"]) == .chat) - #expect(RootTabs.initialTab(arguments: ["OpenClaw", "--openclaw-initial-tab", "voice"]) == .talk) + #expect(RootTabs.initialTab(arguments: ["OpenClaw", "--openclaw-initial-tab", "talk"]) == .chat) + #expect(RootTabs.initialTab(arguments: ["OpenClaw", "--openclaw-initial-tab", "voice"]) == .chat) #expect(RootTabs.initialTab(arguments: ["OpenClaw", "--openclaw-initial-tab", "agents"]) == .agent) #expect(RootTabs.initialTab(arguments: ["OpenClaw", "--openclaw-initial-tab", "settings"]) == .settings) } - @Test func `legacy initial tabs map to matching sidebar destinations`() { + @Test func `initial tabs map to matching sidebar destinations`() { #expect(RootTabs.defaultSidebarDestination(for: .control) == .overview) #expect(RootTabs.defaultSidebarDestination(for: .chat) == .chat) - #expect(RootTabs.defaultSidebarDestination(for: .talk) == .talk) #expect(RootTabs.defaultSidebarDestination(for: .agent) == .agents) #expect(RootTabs.defaultSidebarDestination(for: .settings) == .settings) } @@ -423,27 +421,6 @@ struct RootTabsPresentationTests { #expect(summary.summaryText == "2 dispatched: 1 started, 1 failed.") } - @Test func `talk sidebar destination can receive reveal action`() { - let action = OpenClawSidebarHeaderAction( - systemName: "sidebar.left", - accessibilityLabel: .verbatim("Show Sidebar"), - action: {}) - let routed = TalkProTab(headerLeadingAction: action, openSettings: {}) - let embedded = TalkProTab( - headerLeadingAction: action, - ownsNavigationStack: false, - openSettings: {}) - - #expect(routed.headerLeadingAction?.systemName == "sidebar.left") - guard case let .verbatim(accessibilityLabel)? = routed.headerLeadingAction?.accessibilityLabel else { - Issue.record("expected routed sidebar action to preserve its verbatim accessibility label") - return - } - #expect(accessibilityLabel == "Show Sidebar") - #expect(routed.ownsNavigationStack) - #expect(!embedded.ownsNavigationStack) - } - @Test func `settings can use parent navigation stack for sidebar routes`() { let standalone = SettingsProTab() let embedded = SettingsProTab(ownsNavigationStack: false) diff --git a/apps/ios/Tests/RootTabsSourceGuardTests+GatewaySupport.swift b/apps/ios/Tests/RootTabsSourceGuardTests+GatewaySupport.swift index 824800da3fdb..7b16b03fd69c 100644 --- a/apps/ios/Tests/RootTabsSourceGuardTests+GatewaySupport.swift +++ b/apps/ios/Tests/RootTabsSourceGuardTests+GatewaySupport.swift @@ -145,6 +145,8 @@ extension RootTabsSourceGuardTests { #expect(!settingsSource.contains(".onChange(of: self.showQRScanner)")) #expect(actionsSource.contains("case let .gatewayLink(link):")) #expect(actionsSource.contains("case let .setupCode(code):")) + #expect(actionsSource.contains( + "self.stagedGatewaySetupLink = nil\n self.setupCode = \"\"\n await self.applyGatewayLink(link)")) #expect(stopScanning.lowerBound < deliverResult.lowerBound) #expect(trustSource.contains("Trust this gateway?")) #expect(trustSource.contains("Trust and connect")) @@ -230,7 +232,7 @@ extension RootTabsSourceGuardTests { #expect(connectionFailure.contains("self.localConnectionFailure = message")) #expect(!connectionFailure.contains("self.connectMessage = message")) #expect(connectionFailure.contains("self.statusLine = message")) - #expect(onboardingSource.contains(".failedStatus(message: message, allowsRetry: false)")) + #expect(onboardingSource.contains(".failedStatus(message: localFailure, allowsRetry: false)")) #expect(onboardingSource.contains( "primaryActionTitle: allowsRetry ? OpenClawTextValue.localized(\"Retry\") : nil")) #expect(onboardingSource.contains("onPrimaryAction: allowsRetry ? self.onRetry : nil")) diff --git a/apps/ios/Tests/RootTabsSourceGuardTests.swift b/apps/ios/Tests/RootTabsSourceGuardTests.swift index db4769381f4e..452757059dc9 100644 --- a/apps/ios/Tests/RootTabsSourceGuardTests.swift +++ b/apps/ios/Tests/RootTabsSourceGuardTests.swift @@ -1,7 +1,18 @@ import Foundation +import SwiftUI import Testing +@testable import OpenClaw struct RootTabsSourceGuardTests { + @Test func `unified chat voice tab icon cache follows appearance`() { + let light = UnifiedChatVoiceTabIcon.CacheKey(state: .active, colorScheme: .light) + let sameLight = UnifiedChatVoiceTabIcon.CacheKey(state: .active, colorScheme: .light) + let dark = UnifiedChatVoiceTabIcon.CacheKey(state: .active, colorScheme: .dark) + + #expect(light == sameLight) + #expect(light != dark) + } + @Test func `app applies initial scene phase before gateway admission`() throws { let source = try String(contentsOf: Self.openClawAppSourceURL(), encoding: .utf8) let startupTask = try Self.extract( @@ -82,24 +93,61 @@ struct RootTabsSourceGuardTests { #expect(!drawerContent.contains("NavigationSplitView")) } - @Test func `phone tab bar keeps chat first product order`() throws { + @Test func `phone tab bar keeps chat control agent and settings in product order`() throws { let source = try String(contentsOf: Self.rootTabsSourceURL(), encoding: .utf8) let phoneTabContent = try Self.extract( source, from: "private var phoneTabContent: some View", - to: "private var sidebarSplitContent: some View") + to: "extension RootTabs {\n private func updateCanvasState") let chatRange = try #require(phoneTabContent.range(of: "ChatProTab(")) - let talkRange = try #require(phoneTabContent.range(of: "TalkProTab(")) let controlRange = try #require(phoneTabContent.range(of: "RootTabsPhoneControlHub(")) let agentRange = try #require(phoneTabContent.range(of: "AgentProTab(")) let settingsRange = try #require(phoneTabContent.range(of: "SettingsProTab(")) - #expect(chatRange.lowerBound < talkRange.lowerBound) - #expect(talkRange.lowerBound < controlRange.lowerBound) + #expect(chatRange.lowerBound < controlRange.lowerBound) #expect(controlRange.lowerBound < agentRange.lowerBound) #expect(agentRange.lowerBound < settingsRange.lowerBound) - #expect(phoneTabContent.matches(of: /PhoneTabSettingsHost(?:\([^\n]+\))? \{/).count == 3) + #expect(!phoneTabContent.contains("TalkProTab(")) + #expect(!phoneTabContent.contains(".tag(AppTab.talk)")) + #expect(phoneTabContent.contains("UnifiedChatVoiceTabIcon.image(")) + #expect(phoneTabContent.contains("state: self.phoneChatVoiceIconState")) + #expect(!phoneTabContent.contains("micLevel")) + #expect(!phoneTabContent.contains("playbackLevel")) + #expect(phoneTabContent.contains("self.appModel.talkMode.isEnabled ? \"Chat, voice active\" : \"Chat\"")) + #expect(!phoneTabContent.contains("Label(\"Chat\", systemImage: \"bubble.left.fill\")")) + #expect(phoneTabContent.matches(of: /PhoneTabSettingsHost(?:\([^\n]+\))? \{/).count == 2) + } + + @Test func `phone chat keeps one composer voice control without a tab bar accessory`() throws { + let rootSource = try String(contentsOf: Self.rootTabsSourceURL(), encoding: .utf8) + let chromeSource = try String(contentsOf: Self.rootTabsPhoneChromeSourceURL(), encoding: .utf8) + let chatSource = try String(contentsOf: Self.chatProTabSourceURL(), encoding: .utf8) + + #expect(!rootSource.contains("tabViewBottomAccessory")) + #expect(!rootSource.contains("PhoneVoiceTabAccessory")) + #expect(!chromeSource.contains("PhoneVoiceTabAccessory")) + #expect(chatSource.contains("talkControl: viewModel.isAttachmentOwnerPinned ? nil : self.talkControl")) + #expect(chatSource.contains("private var talkControl: OpenClawChatTalkControl")) + #expect(chatSource.contains("self.appModel.setTalkEnabled(!self.appModel.talkMode.isEnabled)")) + } + + @Test func `composer mic keeps voice notes behind its long press menu`() throws { + let controls = try String(contentsOf: Self.cleanChatComposerControlsSourceURL(), encoding: .utf8) + let composer = try String(contentsOf: Self.sharedChatComposerSourceURL(), encoding: .utf8) + let chat = try String(contentsOf: Self.chatProTabSourceURL(), encoding: .utf8) + + #expect(controls.contains("Label(\"Record Voice Note\", systemImage: \"waveform\")")) + #expect(controls.contains("primaryAction:")) + #expect(controls.contains("struct OpenClawChatMicButton: View")) + #expect(controls.contains("private var isDictationActionEnabled: Bool")) + #expect(controls.contains("isDictationActive: self.dictationControl?.isActive == true")) + #expect(composer.contains("if self.dictationControl != nil || self.voiceNoteControl != nil")) + #expect(composer.contains("isRealtimeTalkActive: self.talkControl?.isEnabled == true")) + #expect(composer.contains("voiceNoteControl: self.voiceNoteControl")) + #expect(composer.contains("embedded: true")) + #expect(chat.contains("voiceNoteControl: self.voiceNoteControl")) + #expect(!chat.contains("OpenClawVoiceNoteButton(")) } @Test func `sidebar keeps navigation model destination only`() throws { @@ -136,7 +184,7 @@ struct RootTabsSourceGuardTests { #expect(source.contains("if self.isSidebarDrawerLayout {")) #expect(!source.contains("private var sidebarFooter: some View")) #expect(!source.contains("LabeledContent(\"Version\"")) - #expect(navigationSource.contains("SidebarGroup(title: \"CHAT\", destinations: [.chat, .talk])")) + #expect(navigationSource.contains("SidebarGroup(title: \"CHAT\", destinations: [.chat])")) #expect(!navigationSource.contains("title: \"AGENT\"")) #expect(navigationSource.contains("case settings")) #expect(!navigationSource.contains("case settingsChannels")) @@ -233,7 +281,6 @@ struct RootTabsSourceGuardTests { let agentDestinationsSource = try String( contentsOf: Self.agentProTabDestinationsSourceURL(), encoding: .utf8) - let talkSource = try String(contentsOf: Self.talkProTabSourceURL(), encoding: .utf8) let settingsSource = try String(contentsOf: Self.settingsProTabSectionsSourceURL(), encoding: .utf8) let overviewSource = try String(contentsOf: Self.commandCenterSourceURL(), encoding: .utf8) let overviewRowsSource = try String(contentsOf: Self.commandCenterSupportSourceURL(), encoding: .utf8) @@ -275,9 +322,6 @@ struct RootTabsSourceGuardTests { #expect(!agentRow.contains("chevron.right")) #expect(agentRow.contains("Image(systemName: \"checkmark\")")) #expect(agentRow.contains("agentAccessibilityLabel")) - #expect(!talkSource.contains("conversationCard")) - #expect(!talkSource.contains("voiceModeCard")) - #expect(!talkSource.contains("statusChip")) #expect(settingsList.contains("Text(\"Device\")")) #expect(settingsList.contains(".font(OpenClawType.captionSemiBold)")) #expect(!settingsList.contains("ProCard(")) @@ -374,7 +418,10 @@ struct RootTabsSourceGuardTests { #expect(docsSource.contains("if !self.usesNativeNavigationChrome")) #expect(overviewSource.contains("OpenClawAdaptiveHeaderRow(")) #expect(overviewSource.matches(of: /if !self\.usesNativeNavigationChrome/).count == 2) - #expect(chatSource.contains(".navigationTitle(self.headerDisplayTitle)")) + #expect(chatSource.contains(".navigationTitle(self.showsAgentBadge ? \"\" : self.headerDisplayTitle)")) + #expect(chatSource.contains("self.headerAgentIdentity")) + #expect(!chatSource.contains("headerAgentModelPicker")) + #expect(chatSource.contains(".sharedBackgroundVisibility(.hidden)")) #expect(chatSource.contains("OpenClawSidebarRevealButton(action: headerLeadingAction)")) #expect(!chatSource.contains("OpenClawAdaptiveHeaderRow(")) #expect(agentOverviewSource.contains("OpenClawAdaptiveHeaderRow(")) @@ -437,7 +484,7 @@ struct RootTabsSourceGuardTests { #expect(!source.contains("safeAreaPadding(.bottom")) } - @Test func `phone hub promotes chat and talk while filtering root tabs from its destination list`() throws { + @Test func `phone hub promotes unified chat while filtering root tabs from its destination list`() throws { let source = try String(contentsOf: Self.phoneHubSourceURL(), encoding: .utf8) #expect(source.contains("private var gatewayHeader: some View")) @@ -445,11 +492,12 @@ struct RootTabsSourceGuardTests { #expect(source.contains("Text(verbatim: gatewayDisplayLabel)")) #expect(source.contains("Text(\"Gateway\")")) #expect(source.contains(".accessibilityLabel(self.gatewayAccessibilityLabel)")) - #expect(source.contains("private var chatTalkRow: some View")) + #expect(source.contains("private var chatShortcut: some View")) #expect(source.contains( ".listRowInsets(EdgeInsets(top: 4, leading: 0, bottom: 4, trailing: 0))")) - #expect(source.contains("self.prominentDestinationCard(\n .chat,")) - #expect(source.contains("self.prominentDestinationCard(\n .talk,")) + #expect(source.contains("self.prominentDestinationCard(\n .chat,")) + #expect(source.contains("Agent chat and recent work.")) + #expect(!source.contains("TalkProTab(")) #expect(source.contains("private var phoneDestinations: [RootTabs.SidebarDestination]")) #expect(source.contains("self.groups.flatMap(\\.destinations).filter { !self.opensRootTab($0) }")) #expect(source.contains("private struct ControlCircleIcon: View")) @@ -780,8 +828,7 @@ extension RootTabsSourceGuardTests { approvalNotificationsRoute.range(of: "self.navigationPath.append(.notifications)")) #expect(rootSource.matches(of: /openSettings: \{ self\.selectSidebarDestination\(\.gateway\) \}/).count >= 2) - #expect(rootSource.matches(of: /openVoiceSettings: \{ openSettingsRoute\(\.voice\) \}/).count == 1) - #expect(rootSource.matches(of: /openVoiceSettings: \{ self\.selectSettingsRoute\(\.voice\) \}/).count == 1) + #expect(!rootSource.contains("openVoiceSettings:")) #expect(rootSource.matches(of: /gatewayAction: \{ self\.selectSidebarDestination\(\.gateway\) \}/).count == 2) #expect(!rootSource.contains("showGatewayActions")) #expect(!rootSource.contains("gatewayActionsDialog")) @@ -794,9 +841,14 @@ extension RootTabsSourceGuardTests { .matches(of: /AgentProTab\([\s\S]*?openSettings: \{ self\.selectSidebarDestination\(\.gateway\) \}/) .count >= 3) #expect(chatSource.contains("let openSettings: (() -> Void)?")) - #expect(chatSource.contains("private var connectionStatusButton: some View")) - #expect(chatSource.contains(".buttonStyle(.plain)")) + #expect(!chatSource.contains("private var connectionStatusButton: some View")) + #expect(!chatSource.contains("private var connectionPill: some View")) + #expect(chatSource.contains("private var gatewayAvatarStatusDot: some View")) + #expect(chatSource.contains(".fill(self.gatewayStatusColor)")) + #expect(chatSource.contains("private var showsExpandedGatewayStatus: Bool")) + #expect(chatSource.contains("Self.gatewayStatusShouldExpand(")) #expect(chatSource.contains(".accessibilityIdentifier(\"chat-gateway-status\")")) + #expect(chatSource.contains(".accessibilityIdentifier(\"chat-gateway-settings\")")) #expect(chatSource.contains("composerChrome: .clean")) #expect(docsSource.contains("let gatewayAction: (() -> Void)?")) #expect(docsSource.contains(".buttonBorderShape(.capsule)")) @@ -1288,7 +1340,7 @@ extension RootTabsSourceGuardTests { #expect(appModelSource.contains("return IOSGatewayChatTransport(")) #expect(appModelSource.contains("globalAgentId: self.chatDeliveryAgentId")) #expect(appModelSource.contains("ifCurrentRoute: operatorRoute")) - #expect(transportSource.matches(of: /ifCurrentRoute: expectedRoute/).count == 3) + #expect(transportSource.matches(of: /ifCurrentRoute: expectedRoute/).count == 4) #expect(channelsSource.contains("\"clickclack\": SettingsChannelFallbackMetadata")) #expect(channelsSource.contains("label: \"ClickClack\"")) #expect(channelsSource.contains("Self-hosted chat bot routing.")) @@ -1643,11 +1695,11 @@ extension RootTabsSourceGuardTests { .appendingPathComponent("Sources/Design/ChatProTab.swift") } - private static func talkProTabSourceURL() -> URL { + private static func rootTabsPhoneChromeSourceURL() -> URL { URL(fileURLWithPath: #filePath) .deletingLastPathComponent() .deletingLastPathComponent() - .appendingPathComponent("Sources/Design/TalkProTab.swift") + .appendingPathComponent("Sources/Design/RootTabsPhoneChrome.swift") } private static func docsSourceURL() -> URL { @@ -1796,6 +1848,22 @@ extension RootTabsSourceGuardTests { .appendingPathComponent("shared/OpenClawKit/Sources/OpenClawChatUI/ChatView+Previews.swift") } + private static func sharedChatComposerSourceURL() -> URL { + URL(fileURLWithPath: #filePath) + .deletingLastPathComponent() + .deletingLastPathComponent() + .deletingLastPathComponent() + .appendingPathComponent("shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift") + } + + private static func cleanChatComposerControlsSourceURL() -> URL { + URL(fileURLWithPath: #filePath) + .deletingLastPathComponent() + .deletingLastPathComponent() + .deletingLastPathComponent() + .appendingPathComponent("shared/OpenClawKit/Sources/OpenClawChatUI/CleanChatComposerControls.swift") + } + private static func xcodeProjectSourceURL() -> URL { URL(fileURLWithPath: #filePath) .deletingLastPathComponent() diff --git a/apps/ios/Tests/RuntimeLocalizationSourceGuardTests.swift b/apps/ios/Tests/RuntimeLocalizationSourceGuardTests.swift index 26be4d955c52..8a62b822435e 100644 --- a/apps/ios/Tests/RuntimeLocalizationSourceGuardTests.swift +++ b/apps/ios/Tests/RuntimeLocalizationSourceGuardTests.swift @@ -3,12 +3,13 @@ import Testing @testable import OpenClaw struct RuntimeLocalizationSourceGuardTests { - @Test func liveActivityStatePersistsSemanticsAndExternalDetail() throws { + @Test func `live activity state persists semantics and external detail`() throws { for status in OpenClawActivityAttributes.ContentState.Status.allCases { let state = OpenClawActivityAttributes.ContentState( status: status, verbatimDetail: status == .attention ? "Backend supplied detail" : nil, - startedAt: Date(timeIntervalSince1970: 1_234)) + startedAt: Date(timeIntervalSince1970: 1234), + voiceSamples: status == .voiceSpeaking ? [12, 96, 240] : nil) let data = try JSONEncoder().encode(state) let decoded = try JSONDecoder().decode(OpenClawActivityAttributes.ContentState.self, from: data) @@ -16,7 +17,7 @@ struct RuntimeLocalizationSourceGuardTests { } } - @Test func liveActivityStateDecodesShippedLegacyPayloads() throws { + @Test func `live activity state decodes shipped legacy payloads`() throws { let cases: [(LegacyContentState, OpenClawActivityAttributes.ContentState.Status, String?)] = [ (LegacyContentState(statusText: "Disconnected", isDisconnected: true), .disconnected, nil), (LegacyContentState(statusText: "Idle", isIdle: true), .idle, nil), @@ -40,7 +41,7 @@ struct RuntimeLocalizationSourceGuardTests { } } - @Test func runtimeOwnedCopyRemainsLocalizableAtRenderTime() throws { + @Test func `runtime owned copy remains localizable at render time`() throws { let attributes = try Self.source("Sources/LiveActivity/OpenClawActivityAttributes.swift") let manager = try Self.source("Sources/LiveActivity/LiveActivityManager.swift") let widget = try Self.source("ActivityWidget/OpenClawLiveActivity.swift") @@ -50,7 +51,6 @@ struct RuntimeLocalizationSourceGuardTests { let proComponents = try Self.source("Sources/Design/OpenClawProComponents.swift") let skillWorkshop = try Self.source("Sources/Design/IPadSkillWorkshopScreen.swift") let workboard = try Self.source("Sources/Design/IPadWorkboardScreen.swift") - let talkPro = try Self.source("Sources/Design/TalkProTab.swift") let talkManager = try Self.source("Sources/Voice/TalkModeManager.swift") let rootTabs = try Self.source("Sources/RootTabs.swift") let rootTabsNavigation = try Self.source("Sources/RootTabsNavigation.swift") @@ -86,7 +86,6 @@ struct RuntimeLocalizationSourceGuardTests { } let destinationTitles = [ "Chat", - "Talk", "Overview", "Activity", "Agents", @@ -117,7 +116,6 @@ struct RuntimeLocalizationSourceGuardTests { skillWorkshop.components(separatedBy: "String(localized: \"Default agent\")").count - 1 == 2) #expect(workboard.components(separatedBy: "String(localized: \"Default agent\")").count - 1 == 4) #expect(!workboard.contains("?? \"Default agent\"")) - #expect(talkPro.contains("if title.isEmpty { return String(localized: \"Not active\") }")) #expect(talkManager.contains( "var gatewayTalkActiveModeTitle: String = .init(localized: \"Not active\")")) for title in [ @@ -133,15 +131,29 @@ struct RuntimeLocalizationSourceGuardTests { #expect(!talkManager.contains("gatewayTalkActiveModeTitle = \"")) } + @Test func `voice waveform stays on avatar without expanded contour`() throws { + let widget = try Self.source("ActivityWidget/OpenClawLiveActivity.swift") + + #expect(!widget.contains("DynamicIslandExpandedRegion(.bottom)")) + #expect(!widget.contains("expandedVoiceContour")) + #expect(widget.contains(".keylineTint(self.islandKeylineTint(state: state))")) + #expect(widget.contains("case .voiceListening, .voiceActive:\n OpenClawActivityStyle.sea")) + #expect(widget.contains("TalkAvatarWaveformView(")) + #expect(widget.contains("Text(\"LIVE\")")) + #expect(!widget.contains("compactVoiceLeading")) + #expect(!widget.contains("compactVoiceTrailing")) + #expect(!widget.contains("Color.clear\n .frame(width: 1, height: 1)")) + } + private static func source(_ path: String) throws -> String { try String( - contentsOf: Self.iosRoot.appendingPathComponent(path), + contentsOf: self.iosRoot.appendingPathComponent(path), encoding: .utf8) } private static func sharedSource(_ path: String) throws -> String { try String( - contentsOf: Self.iosRoot + contentsOf: self.iosRoot .deletingLastPathComponent() .appendingPathComponent("shared/OpenClawKit/Sources") .appendingPathComponent(path), @@ -157,6 +169,6 @@ struct RuntimeLocalizationSourceGuardTests { var isIdle = false var isDisconnected = false var isConnecting = false - var startedAt = Date(timeIntervalSince1970: 1_234) + var startedAt = Date(timeIntervalSince1970: 1234) + } } -} diff --git a/apps/ios/Tests/TalkModeConfigParsingTests.swift b/apps/ios/Tests/TalkModeConfigParsingTests.swift index 72fad0d989fe..2110a7b8161b 100644 --- a/apps/ios/Tests/TalkModeConfigParsingTests.swift +++ b/apps/ios/Tests/TalkModeConfigParsingTests.swift @@ -648,14 +648,15 @@ struct TalkModeManagerTests { #expect(parsed.executionMode == .realtimeRelay) } - @Test func `leaves native mode for unsupported realtime brain`() { + @Test(arguments: ["direct-tools", "none"]) + func `leaves native mode for unsupported realtime brain`(brain: String) { let config: [String: Any] = [ "talk": [ "realtime": [ "provider": "google", "mode": "realtime", "transport": "gateway-relay", - "brain": "direct-tools", + "brain": brain, ], ], ] diff --git a/apps/ios/Tests/TalkProStateTests.swift b/apps/ios/Tests/TalkProStateTests.swift deleted file mode 100644 index 97cd9776a5fc..000000000000 --- a/apps/ios/Tests/TalkProStateTests.swift +++ /dev/null @@ -1,145 +0,0 @@ -import OpenClawChatUI -import Testing -@testable import OpenClaw - -struct TalkProStateTests { - @Test func `disabled talk without loaded config can start and retry load`() { - let state = TalkProState( - gatewayConnected: true, - isDemoMode: false, - isEnabled: false, - phase: .idle, - isConfigLoaded: false, - isListening: false, - isSpeaking: false, - isUserSpeechDetected: false, - permissionState: .unknown) - - #expect(String(localized: state.title) == "Voice config unavailable") - #expect(state.primaryAction == .start) - #expect(String(localized: state.primaryButtonTitle) == "Start Talk") - #expect(state.waveformPhase(micLevel: 0.8, playbackLevel: nil) == .idle) - } - - @Test func `enabled talk without loaded config can be stopped`() { - let state = TalkProState( - gatewayConnected: true, - isDemoMode: false, - isEnabled: true, - phase: .idle, - isConfigLoaded: false, - isListening: false, - isSpeaking: false, - isUserSpeechDetected: false, - permissionState: .unknown) - - #expect(String(localized: state.title) == "Voice config unavailable") - #expect(state.primaryAction == .stop) - #expect(String(localized: state.primaryButtonTitle) == "Stop Talk") - #expect(state.waveformPhase(micLevel: 0.8, playbackLevel: nil) == .idle) - } - - @Test func `enabled talk with loaded config can be stopped`() { - let state = TalkProState( - gatewayConnected: true, - isDemoMode: false, - isEnabled: true, - phase: .idle, - isConfigLoaded: true, - isListening: false, - isSpeaking: false, - isUserSpeechDetected: false, - permissionState: .ready) - - #expect(String(localized: state.title) == "Ready to talk") - #expect(state.primaryAction == .stop) - } - - @Test func `missing scope takes priority over unloaded config`() { - let state = TalkProState( - gatewayConnected: true, - isDemoMode: false, - isEnabled: false, - phase: .idle, - isConfigLoaded: false, - isListening: false, - isSpeaking: false, - isUserSpeechDetected: false, - permissionState: .missingScope("operator.talk.secrets")) - - #expect(String(localized: state.title) == "Gateway permission required") - #expect(state.primaryAction == .enablePermission) - #expect(String(localized: state.primaryButtonTitle) == "Enable Talk") - } - - @Test func `demo mode keeps talk disabled`() { - let state = TalkProState( - gatewayConnected: true, - isDemoMode: true, - isEnabled: true, - phase: .idle, - isConfigLoaded: true, - isListening: true, - isSpeaking: true, - isUserSpeechDetected: true, - permissionState: .ready) - - #expect(String(localized: state.title) == "Demo mode only") - #expect(state.primaryAction == .waiting) - #expect(String(localized: state.primaryButtonTitle) == "Demo Mode Only") - #expect(state.primaryButtonIcon == "lock.fill") - #expect(state.waveformPhase(micLevel: 0.8, playbackLevel: nil) == .idle) - } - - @Test func `listening drives the wave with the real mic level`() { - let state = Self.readyState(isListening: true) - #expect(state.waveformPhase(micLevel: 0.4, playbackLevel: nil) - == .listening(level: 0.4, speechActive: false)) - } - - @Test func `detected speech keeps the real mic level and marks speech active`() { - let state = Self.readyState(isListening: true, isUserSpeechDetected: true) - #expect(state.waveformPhase(micLevel: 0.7, playbackLevel: nil) - == .listening(level: 0.7, speechActive: true)) - } - - @Test func `speaking forwards the playback envelope when available`() { - let state = Self.readyState(isSpeaking: true) - #expect(state.waveformPhase(micLevel: 0, playbackLevel: 0.55) == .speaking(level: 0.55)) - #expect(state.waveformPhase(micLevel: 0, playbackLevel: nil) == .speaking(level: nil)) - } - - @Test @MainActor func `localized status text cannot steer title or waveform`() { - let manager = TalkModeManager(allowSimulatorCapture: true) - manager._test_handleRealtimeRelayStatus("Connecting realtime…") - manager.statusText = "Verbindung wird hergestellt…" - - let connecting = Self.readyState(phase: manager.phase) - #expect(String(localized: connecting.title) == "Connecting") - #expect(connecting.waveformPhase(micLevel: 0, playbackLevel: nil) == .thinking) - - manager.stop() - manager.statusText = "Connecting" - - let idle = Self.readyState(phase: manager.phase) - #expect(String(localized: idle.title) == "Ready to talk") - } - - private static func readyState( - phase: TalkPhase = .idle, - isListening: Bool = false, - isSpeaking: Bool = false, - isUserSpeechDetected: Bool = false) -> TalkProState - { - TalkProState( - gatewayConnected: true, - isDemoMode: false, - isEnabled: true, - phase: phase, - isConfigLoaded: true, - isListening: isListening, - isSpeaking: isSpeaking, - isUserSpeechDetected: isUserSpeechDetected, - permissionState: .ready) - } -} diff --git a/apps/ios/UITests/OpenClawSnapshotUITests.swift b/apps/ios/UITests/OpenClawSnapshotUITests.swift index 3b4a6c2c7564..5a60edc8880e 100644 --- a/apps/ios/UITests/OpenClawSnapshotUITests.swift +++ b/apps/ios/UITests/OpenClawSnapshotUITests.swift @@ -12,9 +12,8 @@ final class OpenClawSnapshotUITests: XCTestCase { private static let screenshotTargets = [ ScreenshotTarget(initialTab: "control", initialDestination: "overview", name: "01-control-connected"), ScreenshotTarget(initialTab: "chat", initialDestination: "chat", name: "02-chat-connected"), - ScreenshotTarget(initialTab: "talk", initialDestination: "talk", name: "03-talk-connected"), - ScreenshotTarget(initialTab: "agent", initialDestination: "agents", name: "04-agent-connected"), - ScreenshotTarget(initialTab: "settings", initialDestination: "settings", name: "05-settings-connected"), + ScreenshotTarget(initialTab: "agent", initialDestination: "agents", name: "03-agent-connected"), + ScreenshotTarget(initialTab: "settings", initialDestination: "settings", name: "04-settings-connected"), ] private var app: XCUIApplication? @@ -214,37 +213,16 @@ final class OpenClawSnapshotUITests: XCTestCase { initialDestination: "chat", name: "chat-settings-back")) - let gatewaySettings = try XCTUnwrap(self.app?.buttons["chat-gateway-status"]) - XCTAssertTrue(gatewaySettings.waitForExistence(timeout: 8)) - gatewaySettings.tap() + try self.openChatGatewaySettings() let gatewayNavigationBar = try XCTUnwrap(self.app?.navigationBars["Gateway"]) XCTAssertTrue(gatewayNavigationBar.waitForExistence(timeout: 5)) XCTAssertTrue(self.app?.tabBars.buttons["Chat"].isSelected == true) self.attachScreenshot(named: "chat-gateway-origin-stack") gatewayNavigationBar.buttons["BackButton"].tap() - XCTAssertTrue(gatewaySettings.waitForExistence(timeout: 5)) + XCTAssertTrue(self.app?.otherElements["chat-agent-identity"].waitForExistence(timeout: 5) == true) XCTAssertTrue(self.app?.tabBars.buttons["Chat"].isSelected == true) self.attachScreenshot(named: "chat-after-settings-back") - - self.launchApp(for: ScreenshotTarget( - initialTab: "talk", - initialDestination: "talk", - name: "talk-settings-back")) - - let voiceSettings = try XCTUnwrap(self.app?.buttons["talk-voice-settings-control"]) - for _ in 0..<3 where !voiceSettings.exists { - self.app?.swipeUp() - } - XCTAssertTrue(voiceSettings.waitForExistence(timeout: 8)) - voiceSettings.tap() - let voiceNavigationBar = try XCTUnwrap(self.app?.navigationBars["Voice & Talk"]) - XCTAssertTrue(voiceNavigationBar.waitForExistence(timeout: 5)) - XCTAssertTrue(self.app?.tabBars.buttons["Talk"].isSelected == true) - - voiceNavigationBar.buttons["BackButton"].tap() - XCTAssertTrue(voiceSettings.waitForExistence(timeout: 5)) - XCTAssertTrue(self.app?.tabBars.buttons["Talk"].isSelected == true) } func testVoiceWakeResumesAfterTalkModeToggle() throws { @@ -323,21 +301,38 @@ final class OpenClawSnapshotUITests: XCTestCase { XCTAssertTrue(talkButton.waitForExistence(timeout: 5)) let attachmentButton = try XCTUnwrap(app?.buttons["chat-attachment-picker"]) XCTAssertTrue(attachmentButton.waitForExistence(timeout: 5)) + let dictationButton = try XCTUnwrap(app?.buttons["chat-dictation-control"]) + XCTAssertTrue(dictationButton.waitForExistence(timeout: 5)) let composerSurface = try XCTUnwrap(app?.otherElements["chat-composer-surface"]) XCTAssertTrue(composerSurface.waitForExistence(timeout: 5)) - let gatewayStatus = try XCTUnwrap(app?.buttons["chat-gateway-status"]) - XCTAssertTrue(gatewayStatus.waitForExistence(timeout: 5)) + let agentIdentity = try XCTUnwrap(app?.otherElements["chat-agent-identity"]) + XCTAssertTrue(agentIdentity.waitForExistence(timeout: 5)) + XCTAssertEqual(agentIdentity.value as? String, "Collapsed") + agentIdentity.tap() + self.waitForValue("Expanded", of: agentIdentity) let sendButton = try XCTUnwrap(app?.buttons["chat-send-message"]) XCTAssertFalse(sendButton.exists) - XCTAssertLessThanOrEqual(attachmentButton.frame.maxX, composerSurface.frame.minX) - XCTAssertLessThanOrEqual(composerSurface.frame.maxX, talkButton.frame.minX) + XCTAssertLessThanOrEqual(agentIdentity.frame.maxY, composerSurface.frame.minY) + XCTAssertGreaterThanOrEqual(attachmentButton.frame.minX, composerSurface.frame.minX) + XCTAssertLessThanOrEqual(attachmentButton.frame.maxX, composerSurface.frame.maxX) + XCTAssertGreaterThanOrEqual(dictationButton.frame.minX, composerSurface.frame.minX) + XCTAssertLessThanOrEqual(dictationButton.frame.maxX, composerSurface.frame.maxX) + XCTAssertGreaterThanOrEqual(talkButton.frame.minX, composerSurface.frame.minX) + XCTAssertLessThanOrEqual(talkButton.frame.maxX, composerSurface.frame.maxX) XCTAssertGreaterThanOrEqual(attachmentButton.frame.width, 44) XCTAssertGreaterThanOrEqual(attachmentButton.frame.height, 44) + XCTAssertGreaterThanOrEqual(dictationButton.frame.width, 44) + XCTAssertGreaterThanOrEqual(dictationButton.frame.height, 44) XCTAssertGreaterThanOrEqual(talkButton.frame.width, 44) XCTAssertGreaterThanOrEqual(talkButton.frame.height, 44) let compactHeight = textField.frame.height XCTAssertLessThanOrEqual(compactHeight, 44) - XCTAssertLessThanOrEqual(abs(talkButton.frame.midY - textField.frame.midY), 1) + XCTAssertLessThanOrEqual(abs(attachmentButton.frame.midY - dictationButton.frame.midY), 1) + XCTAssertLessThanOrEqual(abs(talkButton.frame.midY - dictationButton.frame.midY), 1) + + attachmentButton.tap() + XCTAssertFalse(self.app?.buttons["Voice Memo"].exists == true) + self.app?.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.2)).tap() self.attachScreenshot(named: "chat-composer-compact") textField.tap() @@ -367,7 +362,7 @@ final class OpenClawSnapshotUITests: XCTestCase { name: "chat-light"), appearance: "light") - XCTAssertTrue(self.app?.buttons["chat-gateway-status"].waitForExistence(timeout: 8) == true) + XCTAssertTrue(self.app?.otherElements["chat-agent-identity"].waitForExistence(timeout: 8) == true) XCTAssertTrue(self.app?.otherElements["chat-composer-surface"].exists == true) self.attachScreenshot(named: "chat-light") } @@ -486,40 +481,6 @@ final class OpenClawSnapshotUITests: XCTestCase { XCTAssertTrue(app.tabBars.buttons["Chat"].isSelected) } - func testTalkUsesNativeControls() throws { - try XCTSkipIf(UIDevice.current.userInterfaceIdiom != .phone, "Phone Talk controls only") - self.launchApp(for: ScreenshotTarget( - initialTab: "talk", - initialDestination: "talk", - name: "talk-native-controls")) - - let speakerphone = try XCTUnwrap(app?.switches["talk-speakerphone-control"]) - let backgroundListening = try XCTUnwrap(app?.switches["talk-background-listening-control"]) - let voiceSettings = try XCTUnwrap(app?.buttons["talk-voice-settings-control"]) - for _ in 0..<3 where !speakerphone.exists { - self.app?.swipeUp() - } - XCTAssertTrue(speakerphone.waitForExistence(timeout: 8)) - XCTAssertTrue(backgroundListening.exists) - XCTAssertTrue(voiceSettings.exists) - - let originalValue = speakerphone.value as? String - defer { - if speakerphone.value as? String != originalValue { - speakerphone.coordinate(withNormalizedOffset: CGVector(dx: 0.9, dy: 0.5)).tap() - } - } - if originalValue == "0" { - speakerphone.coordinate(withNormalizedOffset: CGVector(dx: 0.9, dy: 0.5)).tap() - self.waitForValue("1", of: speakerphone) - } - XCTAssertEqual(speakerphone.value as? String, "1") - self.attachScreenshot(named: "talk-native-controls") - - speakerphone.coordinate(withNormalizedOffset: CGVector(dx: 0.9, dy: 0.5)).tap() - self.waitForValue("0", of: speakerphone) - } - func testAppearanceUsesSettingsRow() throws { self.launchApp(for: ScreenshotTarget( initialTab: "settings", @@ -581,9 +542,7 @@ final class OpenClawSnapshotUITests: XCTestCase { // Retain an embedded Chat Settings route, then prove contextual routing pops it. chatTab.tap() - let gatewaySettings = try XCTUnwrap(self.app?.buttons["chat-gateway-status"]) - XCTAssertTrue(gatewaySettings.waitForExistence(timeout: 5)) - gatewaySettings.tap() + try self.openChatGatewaySettings() XCTAssertTrue(self.app?.navigationBars["Gateway"].waitForExistence(timeout: 5) == true) controlTab.tap() @@ -959,6 +918,22 @@ final class OpenClawSnapshotUITests: XCTestCase { XCTAssertTrue(app.staticTexts["Writing"].waitForNonExistence(timeout: 5)) } + private func openChatGatewaySettings( + file: StaticString = #filePath, + line: UInt = #line) throws + { + let actions = try XCTUnwrap(self.app?.buttons["Chat actions"], file: file, line: line) + XCTAssertTrue(actions.waitForExistence(timeout: 8), file: file, line: line) + actions.tap() + + let gatewaySettings = try XCTUnwrap( + self.app?.buttons["chat-gateway-settings"], + file: file, + line: line) + XCTAssertTrue(gatewaySettings.waitForExistence(timeout: 3), file: file, line: line) + gatewaySettings.tap() + } + private func assertDarkAppearanceTextVisible( file: StaticString = #filePath, line: UInt = #line) diff --git a/apps/ios/project.yml b/apps/ios/project.yml index ee28e9ebdada..ea6c21770063 100644 --- a/apps/ios/project.yml +++ b/apps/ios/project.yml @@ -262,6 +262,9 @@ targets: sources: - path: ActivityWidget - path: Sources/LiveActivity/OpenClawActivityAttributes.swift + # Shared continuous avatar waveform; dependency-free for widget isolation. + - path: ../shared/OpenClawKit/Sources/OpenClawChatUI/TalkWaveformView.swift + group: Shared - path: Sources/Fonts buildPhase: resources - path: Resources/Localizable.xcstrings diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift index 66e7bc90e831..fa9bc7a4170c 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift @@ -3,52 +3,29 @@ import Observation import SwiftUI import UniformTypeIdentifiers -#if !os(macOS) -import PhotosUI +#if os(macOS) +import AppKit #endif -private struct CleanChatComposerSurface: ViewModifier { - let cornerRadius: CGFloat +#if !os(macOS) +import PhotosUI +#if canImport(UIKit) +import UIKit - func body(content: Content) -> some View { - #if os(macOS) - content - .background( - RoundedRectangle(cornerRadius: self.cornerRadius, style: .continuous) - .fill(OpenClawChatTheme.composerField)) - .overlay( - RoundedRectangle(cornerRadius: self.cornerRadius, style: .continuous) - .strokeBorder(OpenClawChatTheme.composerBorder, lineWidth: 1)) - #else - if #available(iOS 26.0, *) { - content - .glassEffect(.regular, in: .rect(cornerRadius: self.cornerRadius)) - } else { - content - .background( - .regularMaterial, - in: RoundedRectangle(cornerRadius: self.cornerRadius, style: .continuous)) - .overlay( - RoundedRectangle(cornerRadius: self.cornerRadius, style: .continuous) - .strokeBorder(OpenClawChatTheme.composerBorder, lineWidth: 1)) - } - #endif - } +/// The camera hands us an immutable snapshot, but UIKit does not expose it as +/// Sendable. This wrapper is only used for one detached JPEG encoding pass. +private struct OpenClawSendableCameraImage: @unchecked Sendable { + let value: UIImage +} +#endif + +@MainActor +private struct OpenClawChatAttachmentCaptureOwner { + let viewModel: OpenClawChatViewModel + let session: OpenClawChatViewModel.SessionSnapshot } -private enum CleanChatComposerMetrics { - static let controlHeight: CGFloat = 44 -} - -private struct CompactChatAttachmentLabel: View { - var body: some View { - Image(systemName: "paperclip") - .font(OpenClawChatTypography.display(size: 15, weight: .semibold, relativeTo: .subheadline)) - .foregroundStyle(.secondary) - .frame(width: CleanChatComposerMetrics.controlHeight, height: CleanChatComposerMetrics.controlHeight) - .contentShape(Rectangle()) - } -} +#endif private struct SlashPanelHeightKey: PreferenceKey { static let defaultValue: CGFloat = 0 @@ -57,6 +34,17 @@ private struct SlashPanelHeightKey: PreferenceKey { } } +struct OpenClawChatComposerPresentationOwner: Equatable { + let viewModelID: ObjectIdentifier + let session: OpenClawChatViewModel.SessionSnapshot + + @MainActor + init(viewModel: OpenClawChatViewModel) { + self.viewModelID = ObjectIdentifier(viewModel) + self.session = viewModel.currentSessionSnapshot() + } +} + @MainActor struct OpenClawChatComposer: View { @Bindable var viewModel: OpenClawChatViewModel @@ -71,14 +59,26 @@ struct OpenClawChatComposer: View { let isAttachmentInputEnabled: Bool let messagePlaceholder: String? let talkControl: OpenClawChatTalkControl? + let dictationControl: OpenClawChatDictationControl? let voiceNoteControl: OpenClawChatVoiceNoteControl? @State private var isSlashPopoverPresented = false @State private var suppressNextSlashPopoverUpdate = false @State private var slashPanelHeight: CGFloat = 0 @State private var slashHighlightIndex = 0 + @State private var dictationTask: Task? #if !os(macOS) @State private var pickerItems: [PhotosPickerItem] = [] + @State private var showsPhotoPicker = false + @State private var showsFileImporter = false + @State private var showsCameraPicker = false + @State private var photoPickerOwner: OpenClawChatAttachmentCaptureOwner? + @State private var fileImporterOwner: OpenClawChatAttachmentCaptureOwner? + #if canImport(UIKit) + @State private var cameraEncodingTask: Task? + @State private var cameraEncodingGeneration = UUID() + @State private var cameraCaptureOwner: OpenClawChatAttachmentCaptureOwner? + #endif @FocusState private var isFocused: Bool #else @State private var shouldFocusTextView = false @@ -86,105 +86,269 @@ struct OpenClawChatComposer: View { @ScaledMetric(relativeTo: .body) private var scaledBodyLineHeight: CGFloat = 22 var body: some View { - VStack(alignment: .leading, spacing: 4) { - if self.showsToolbar, self.voiceNoteControl?.recorder.isRecording != true { - self.composerToolbar - } + self.lifecycleComposer + } - if self.showsAttachments, !self.viewModel.attachments.isEmpty { - self.attachmentsStrip - } + private var styledComposer: some View { + self.composerContent + .padding(composerPadding) + .background { self.composerBackground } + } - if let replyTarget = self.viewModel.replyTarget { - ChatReplyPreview(target: replyTarget) { - self.viewModel.clearReplyTarget() - } + #if os(macOS) + private var platformComposer: some View { + self.styledComposer + .onDrop(of: [.fileURL], isTargeted: nil) { providers in + self.handleDrop(providers) } - - if let talkControl, talkControl.isEnabled { - ChatTalkActivityStrip(control: talkControl) + .onAppear { + self.shouldFocusTextView = true + self.viewModel.loadSlashCommandsIfNeeded() } - - if let voiceNoteControl, voiceNoteControl.recorder.isRecording { - OpenClawVoiceNoteRecordingRow(recorder: voiceNoteControl.recorder) - .padding(self.editorPadding) - } else { - self.editor - } - } - .padding(self.composerPadding) - .background { - if self.composerChrome == .full { - let cornerRadius: CGFloat = 18 - - #if os(macOS) - if self.style == .standard { - let shape = UnevenRoundedRectangle( - cornerRadii: RectangleCornerRadii( - topLeading: 0, - bottomLeading: cornerRadius, - bottomTrailing: cornerRadius, - topTrailing: 0), - style: .continuous) - shape - .fill(OpenClawChatTheme.composerBackground) - .overlay(shape.strokeBorder(OpenClawChatTheme.composerBorder, lineWidth: 1)) - .shadow(color: .black.opacity(0.12), radius: 12, y: 6) - } else { - let shape = RoundedRectangle(cornerRadius: cornerRadius, style: .continuous) - shape - .fill(OpenClawChatTheme.composerBackground) - .overlay(shape.strokeBorder(OpenClawChatTheme.composerBorder, lineWidth: 1)) - .shadow(color: .black.opacity(0.12), radius: 12, y: 6) - } - #else - let shape = RoundedRectangle(cornerRadius: cornerRadius, style: .continuous) - shape - .fill(OpenClawChatTheme.composerBackground) - .overlay(shape.strokeBorder(OpenClawChatTheme.composerBorder, lineWidth: 1)) - .shadow(color: .black.opacity(0.12), radius: 12, y: 6) - #endif - } - } - #if os(macOS) - .onDrop(of: [.fileURL], isTargeted: nil) { providers in - self.handleDrop(providers) - } - .onAppear { - self.shouldFocusTextView = true - self.viewModel.loadSlashCommandsIfNeeded() - } - #else - .onChange(of: self.isComposerEnabled) { _, isEnabled in + } + #else + private var platformComposer: some View { + self.styledComposer + .onChange(of: self.isComposerEnabled) { _, isEnabled in if !isEnabled { self.isFocused = false self.setSlashPanelPresented(false) } } + .onChange(of: self.isAttachmentInputEnabled) { _, isEnabled in + if !isEnabled { + self.showsPhotoPicker = false + self.showsFileImporter = false + self.showsCameraPicker = false + self.photoPickerOwner = nil + self.fileImporterOwner = nil + #if canImport(UIKit) + self.cameraCaptureOwner = nil + #endif + self.cancelActiveCameraEncoding() + } + } .onAppear { self.viewModel.loadSlashCommandsIfNeeded() } - #endif - .onChange(of: self.voiceNoteControl?.recorder.completedRecording) { _, recording in - guard recording != nil else { return } - self.stageCompletedVoiceNoteIfNeeded() - } - .onChange(of: self.voiceNoteControl?.recorder.ownsPendingChatAttachment) { _, _ in - self.viewModel.attachmentOwnerActivityChanged() - } - .onChange(of: self.voiceNoteControl?.recorder.errorMessage) { _, message in - if let message { - self.viewModel.errorText = message + .fileImporter( + isPresented: self.fileImporterPresentation, + allowedContentTypes: [.image], + allowsMultipleSelection: true, + onCompletion: { result in + let owner = self.fileImporterOwner + self.fileImporterOwner = nil + self.handleFileImport(result, owner: owner) + }) + .photosPicker( + isPresented: self.photoPickerPresentation, + selection: self.$pickerItems, + maxSelectionCount: 8, + matching: .images) + .onChange(of: self.pickerItems) { _, items in + guard !items.isEmpty else { return } + let owner = self.photoPickerOwner + self.photoPickerOwner = nil + self.stagePhotosPickerItems(items, owner: owner) + } + #if canImport(UIKit) + .fullScreenCover( + isPresented: self.cameraPickerPresentation, + onDismiss: { self.cameraCaptureOwner = nil }, + content: { + let owner = self.cameraCaptureOwner + OpenClawChatCameraPicker { image in + guard let owner else { return } + self.addCameraImage(image, owner: owner) } + .ignoresSafeArea() + }) + #endif + } + #endif + + private var recorderLifecycleComposer: some View { + self.platformComposer + .onChange(of: self.voiceNoteControl?.recorder.completedRecording) { _, recording in + guard recording != nil else { return } + self.stageCompletedVoiceNoteIfNeeded() + } + .onChange(of: self.voiceNoteControl?.recorder.ownsPendingChatAttachment) { _, _ in + self.viewModel.attachmentOwnerActivityChanged() + } + .onChange(of: self.voiceNoteControl?.recorder.errorMessage) { _, message in + if let message { + self.viewModel.errorText = message } - .onAppear { - self.viewModel.attachmentOwnerActivityChanged() - self.stageCompletedVoiceNoteIfNeeded() + } + } + + private var lifecycleComposer: some View { + self.recorderLifecycleComposer + .onChange(of: self.presentationOwner) { _, _ in + self.cancelActiveDictation() + #if !os(macOS) + self.showsPhotoPicker = false + self.showsFileImporter = false + self.showsCameraPicker = false + self.photoPickerOwner = nil + self.fileImporterOwner = nil + #if canImport(UIKit) + self.cameraCaptureOwner = nil + #endif + self.cancelActiveCameraEncoding() + #endif + } + .onAppear { + self.viewModel.attachmentOwnerActivityChanged() + self.stageCompletedVoiceNoteIfNeeded() + } + .onDisappear { + self.cancelActiveDictation() + #if !os(macOS) + self.showsPhotoPicker = false + self.showsFileImporter = false + self.showsCameraPicker = false + self.photoPickerOwner = nil + self.fileImporterOwner = nil + #if canImport(UIKit) + self.cameraCaptureOwner = nil + #endif + self.cancelActiveCameraEncoding() + #endif + self.cancelActiveVoiceNoteIfNeeded() + self.viewModel.attachmentOwnerActivityChanged() + } + } + + private var composerContent: some View { + VStack(alignment: .leading, spacing: 4) { + if self.composerChrome == .clean { + self.cleanComposerCard + } else { + if self.showsToolbar, self.voiceNoteControl?.recorder.isRecording != true { + self.composerToolbar } - .onDisappear { - self.cancelActiveVoiceNoteIfNeeded() - self.viewModel.attachmentOwnerActivityChanged() + + if self.showsAttachments, !self.viewModel.attachments.isEmpty { + self.attachmentsStrip } + + self.composerContextRows + + if let voiceNoteControl, voiceNoteControl.recorder.isRecording { + OpenClawVoiceNoteRecordingRow(recorder: voiceNoteControl.recorder) + .padding(self.editorPadding) + } else { + self.editor + } + } + } + } + + @ViewBuilder + private var composerContextRows: some View { + if let replyTarget = self.viewModel.replyTarget { + ChatReplyPreview(target: replyTarget) { + self.viewModel.clearReplyTarget() + } + } + + if let talkControl, talkControl.isEnabled { + ChatTalkActivityStrip(control: talkControl) + } + } + + private var presentationOwner: OpenClawChatComposerPresentationOwner { + OpenClawChatComposerPresentationOwner(viewModel: self.viewModel) + } + + #if !os(macOS) + private func attachmentCaptureOwner() -> OpenClawChatAttachmentCaptureOwner { + OpenClawChatAttachmentCaptureOwner( + viewModel: self.viewModel, + session: self.viewModel.currentSessionSnapshot()) + } + + private var photoPickerPresentation: Binding { + Binding( + get: { self.showsPhotoPicker }, + set: { isPresented in + if isPresented { + self.photoPickerOwner = self.attachmentCaptureOwner() + } + self.showsPhotoPicker = isPresented + }) + } + + private func presentPhotoPicker() { + self.photoPickerOwner = self.attachmentCaptureOwner() + self.showsPhotoPicker = true + } + + private var fileImporterPresentation: Binding { + Binding( + get: { self.showsFileImporter }, + set: { isPresented in + if isPresented { + self.fileImporterOwner = self.attachmentCaptureOwner() + } + self.showsFileImporter = isPresented + }) + } + + private var cameraPickerPresentation: Binding { + Binding( + get: { self.showsCameraPicker }, + set: { isPresented in + #if canImport(UIKit) + if isPresented { + self.cameraCaptureOwner = self.attachmentCaptureOwner() + } + #endif + self.showsCameraPicker = isPresented + #if canImport(UIKit) + if !isPresented { + self.cameraCaptureOwner = nil + } + #endif + }) + } + #endif + + @ViewBuilder + private var composerBackground: some View { + if self.composerChrome == .full { + let cornerRadius: CGFloat = 18 + + #if os(macOS) + if self.style == .standard { + let shape = UnevenRoundedRectangle( + cornerRadii: RectangleCornerRadii( + topLeading: 0, + bottomLeading: cornerRadius, + bottomTrailing: cornerRadius, + topTrailing: 0), + style: .continuous) + shape + .fill(OpenClawChatTheme.composerBackground) + .overlay(shape.strokeBorder(OpenClawChatTheme.composerBorder, lineWidth: 1)) + .shadow(color: .black.opacity(0.12), radius: 12, y: 6) + } else { + let shape = RoundedRectangle(cornerRadius: cornerRadius, style: .continuous) + shape + .fill(OpenClawChatTheme.composerBackground) + .overlay(shape.strokeBorder(OpenClawChatTheme.composerBorder, lineWidth: 1)) + .shadow(color: .black.opacity(0.12), radius: 12, y: 6) + } + #else + let shape = RoundedRectangle(cornerRadius: cornerRadius, style: .continuous) + shape + .fill(OpenClawChatTheme.composerBackground) + .overlay(shape.strokeBorder(OpenClawChatTheme.composerBorder, lineWidth: 1)) + .shadow(color: .black.opacity(0.12), radius: 12, y: 6) + #endif + } } private var composerToolbar: some View { @@ -447,7 +611,9 @@ struct OpenClawChatComposer: View { } #else if self.composerChrome == .clean { - PhotosPicker(selection: self.$pickerItems, maxSelectionCount: 8, matching: .images) { + Button { + self.presentPhotoPicker() + } label: { CompactChatAttachmentLabel() } .help("Add Image") @@ -456,11 +622,10 @@ struct OpenClawChatComposer: View { .buttonStyle(.plain) .controlSize(.small) .disabled(!self.isAttachmentInputEnabled) - .onChange(of: self.pickerItems) { _, newItems in - Task { await self.loadPhotosPickerItems(newItems) } - } } else { - PhotosPicker(selection: self.$pickerItems, maxSelectionCount: 8, matching: .images) { + Button { + self.presentPhotoPicker() + } label: { Image(systemName: "paperclip") } .help("Add Image") @@ -468,63 +633,27 @@ struct OpenClawChatComposer: View { .buttonStyle(.bordered) .controlSize(.small) .disabled(!self.isAttachmentInputEnabled) - .onChange(of: self.pickerItems) { _, newItems in - Task { await self.loadPhotosPickerItems(newItems) } - } } #endif } + @ViewBuilder + private var cleanAttachmentMenu: some View { + #if os(macOS) + self.attachmentPicker + #else + OpenClawChatAttachmentMenu( + showsPhotoPicker: self.photoPickerPresentation, + showsFileImporter: self.fileImporterPresentation, + showsCameraPicker: self.cameraPickerPresentation, + isAttachmentInputEnabled: self.isAttachmentInputEnabled) + #endif + } + private var attachmentsStrip: some View { - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 6) { - ForEach( - self.viewModel.attachments, - id: \OpenClawPendingAttachment.id) - { (att: OpenClawPendingAttachment) in - HStack(spacing: 6) { - if let img = att.preview { - OpenClawPlatformImageFactory.image(img) - .resizable() - .scaledToFill() - .frame(width: 22, height: 22) - .clipShape(RoundedRectangle(cornerRadius: 6, style: .continuous)) - } else if att.mimeType.hasPrefix("audio/") { - Image(systemName: "waveform") - Text("Voice note") - .font(OpenClawChatTypography.caption) - if let durationSeconds = att.durationSeconds { - Text(openClawVoiceNoteDurationLabel(durationSeconds)) - .font(OpenClawChatTypography.caption) - .foregroundStyle(.secondary) - } - } else { - Image(systemName: "photo") - Text(att.fileName) - .font(OpenClawChatTypography.caption) - .lineLimit(1) - } - - if att.preview != nil { - Text(att.fileName) - .font(OpenClawChatTypography.caption) - .lineLimit(1) - } - - Button { - self.viewModel.removeAttachment(att.id) - } label: { - Image(systemName: "xmark.circle.fill") - } - .buttonStyle(.plain) - } - .padding(.horizontal, 8) - .padding(.vertical, 5) - .background(OpenClawChatTheme.accent.opacity(0.08)) - .clipShape(Capsule()) - } - } - } + OpenClawChatAttachmentsStrip( + attachments: self.viewModel.attachments, + onRemove: { self.viewModel.removeAttachment($0) }) } private var editor: some View { @@ -584,59 +713,93 @@ struct OpenClawChatComposer: View { .overlay( RoundedRectangle(cornerRadius: 14, style: .continuous) .strokeBorder(OpenClawChatTheme.composerBorder))) - .padding(self.editorPadding) + .padding(editorPadding) + } + + private var cleanComposerCard: some View { + VStack(alignment: .leading, spacing: 8) { + if self.showsAttachments, !self.viewModel.attachments.isEmpty { + self.attachmentsStrip + } + + self.composerContextRows + + if let voiceNoteControl, voiceNoteControl.recorder.isRecording { + OpenClawVoiceNoteRecordingRow( + recorder: voiceNoteControl.recorder, + embedded: true) + } else { + self.editor + } + } + .padding(.horizontal, 6) + .padding(.vertical, 2) + .modifier(CleanChatComposerSurface(cornerRadius: 24)) + .accessibilityElement(children: .contain) + .accessibilityIdentifier("chat-composer-surface") } private var cleanEditor: some View { - VStack(alignment: .leading, spacing: 6) { - // Bottom-aligned so the paperclip and send/mic stay on the last - // text line when the field grows to multiple lines, like iMessage. - HStack(alignment: .bottom, spacing: 8) { - self.attachmentPicker - .frame(width: self.cleanIconControlSize, height: self.cleanEditorMinHeight) + HStack(alignment: .center, spacing: 4) { + self.cleanAttachmentMenu - if let voiceNoteControl, !voiceNoteControl.isTalkActive { - OpenClawVoiceNoteButton( - control: voiceNoteControl, - compact: true, - isComposerEnabled: self.isComposerEnabled, - isAttachmentInputEnabled: self.isAttachmentInputEnabled) - .frame(width: self.cleanIconControlSize, height: self.cleanEditorMinHeight) - } + self.editorOverlay + .frame(maxWidth: .infinity, minHeight: self.cleanControlHeight, alignment: .leading) + .layoutPriority(1) - self.editorOverlay - .padding(.vertical, self.cleanEditorTextPadding) - .padding(.horizontal, 14) - .frame(minHeight: self.cleanEditorMinHeight) - .modifier(CleanChatComposerSurface(cornerRadius: self.cleanEditorCornerRadius)) - .accessibilityElement(children: .contain) - .accessibilityIdentifier("chat-composer-surface") - - self.cleanTrailingControl - .frame(height: self.cleanEditorMinHeight, alignment: .bottom) + if self.dictationControl != nil || self.voiceNoteControl != nil { + OpenClawChatMicButton( + dictationControl: self.dictationControl, + voiceNoteControl: self.voiceNoteControl, + isDictationPending: self.dictationTask != nil, + isRealtimeTalkActive: self.talkControl?.isEnabled == true, + isComposerEnabled: self.isComposerEnabled, + isAttachmentInputEnabled: self.isAttachmentInputEnabled, + onStartDictation: { + if let dictationControl = self.dictationControl { + self.startDictation(dictationControl) + } + }) } - if self.showsConnectionPill { - self.connectionPill - .padding(.leading, 44) - } + self.cleanTrailingControl } - .padding(.horizontal, 14) - .padding(.vertical, 4) } /// iMessage-style trailing control: the talk (mic) affordance while the /// draft is empty, swapping to the send button once the user types. @ViewBuilder private var cleanTrailingControl: some View { - if !self.viewModel.hasDraftToSend, !self.viewModel.hasBlockingRunActivity, let talkControl { + if Self.showsCompactTalkControl( + hasDraftToSend: self.viewModel.hasDraftToSend, + hasBlockingRunActivity: self.viewModel.hasBlockingRunActivity, + isLocalVoiceCaptureActive: self.isLocalVoiceCaptureActive), + let talkControl + { self.compactTalkButton(talkControl) } else { - self.sendButton - .frame(width: self.cleanControlHeight, height: self.cleanControlHeight) + sendButton + .frame(width: cleanControlHeight, height: cleanControlHeight) } } + private var isLocalVoiceCaptureActive: Bool { + self.dictationTask != nil || + self.dictationControl?.isActive == true || + self.voiceNoteControl?.recorder.isRecording == true || + self.voiceNoteControl?.recorder.isRequestingPermission == true + } + + /// Local capture and realtime Talk share one microphone, so the Talk affordance + /// must yield until dictation or voice-note capture releases audio ownership. + nonisolated static func showsCompactTalkControl( + hasDraftToSend: Bool, + hasBlockingRunActivity: Bool, + isLocalVoiceCaptureActive: Bool) -> Bool + { + !hasDraftToSend && !hasBlockingRunActivity && !isLocalVoiceCaptureActive + } + private func talkButton(_ talkControl: OpenClawChatTalkControl) -> some View { Button { talkControl.toggle(self.viewModel.sessionKey) @@ -669,6 +832,30 @@ struct OpenClawChatComposer: View { .chatTalkInputDeviceMenu(talkControl) } + private func startDictation(_ control: OpenClawChatDictationControl) { + guard self.dictationTask == nil else { return } + let session = self.viewModel.currentSessionSnapshot() + self.dictationTask = Task { @MainActor in + defer { self.dictationTask = nil } + do { + guard let transcript = try await control.start(), !transcript.isEmpty else { return } + try Task.checkCancellation() + self.viewModel.appendDictationTranscript(transcript, for: session) + } catch is CancellationError { + return + } catch { + guard !Task.isCancelled else { return } + self.viewModel.setDictationError(error, for: session) + } + } + } + + private func cancelActiveDictation() { + guard self.dictationTask != nil else { return } + self.dictationTask?.cancel() + self.dictationControl?.cancel() + } + private func compactTalkButton(_ talkControl: OpenClawChatTalkControl) -> some View { Button { talkControl.toggle(self.viewModel.sessionKey) @@ -755,7 +942,7 @@ struct OpenClawChatComposer: View { } private var editorOverlay: some View { - ZStack(alignment: self.editorOverlayAlignment) { + ZStack(alignment: editorOverlayAlignment) { if self.viewModel.input.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { Text(self.placeholderText) .font(OpenClawChatTypography.body) @@ -1181,18 +1368,6 @@ extension OpenClawChatComposer { return max(base, self.scaledBodyLineHeight * 4) } - private var cleanEditorMinHeight: CGFloat { - max(44, self.textMinHeight + self.cleanEditorTextPadding * 2) - } - - private var cleanEditorCornerRadius: CGFloat { - self.cleanEditorMinHeight / 2 - } - - private var cleanEditorTextPadding: CGFloat { - 10 - } - private var sendButtonSize: CGFloat { self.composerChrome == .clean ? self.cleanControlHeight : 44 } @@ -1245,13 +1420,16 @@ extension OpenClawChatComposer { private var canSendMessage: Bool { self.isComposerEnabled + // Dictation appends when capture completes; sending mid-capture + // would split one draft across two messages. + && self.dictationTask == nil && self.voiceNoteControl?.recorder.ownsPendingChatAttachment != true && self.viewModel.canSend && (self.isAttachmentInputEnabled || self.viewModel.attachments.isEmpty) } private func stageCompletedVoiceNoteIfNeeded() { - guard let recorder = self.voiceNoteControl?.recorder, + guard let recorder = voiceNoteControl?.recorder, let recording = recorder.claimCompletedRecording() else { return } @@ -1265,7 +1443,7 @@ extension OpenClawChatComposer { } private func cancelActiveVoiceNoteIfNeeded() { - guard let recorder = self.voiceNoteControl?.recorder, + guard let recorder = voiceNoteControl?.recorder, recorder.isRecording || recorder.isRequestingPermission else { return } // The app-owned recorder outlives this view. Release the microphone @@ -1317,21 +1495,120 @@ extension OpenClawChatComposer { return true } #else - private func loadPhotosPickerItems(_ items: [PhotosPickerItem]) async { + private func handleFileImport( + _ result: Result<[URL], Error>, + owner: OpenClawChatAttachmentCaptureOwner?) + { + guard self.isAttachmentInputEnabled else { return } + guard let owner, + self.viewModel === owner.viewModel, + owner.viewModel.isCurrentSession(owner.session) + else { return } + switch result { + case let .success(urls): + owner.viewModel.addAttachments(urls: urls, for: owner.session) + case let .failure(error): + if !(error is CancellationError) { + owner.viewModel.errorText = error.localizedDescription + } + } + } + + #if canImport(UIKit) + private func addCameraImage(_ image: UIImage, owner: OpenClawChatAttachmentCaptureOwner) { + guard self.isAttachmentInputEnabled else { return } + guard self.viewModel === owner.viewModel, + owner.viewModel.isCurrentSession(owner.session) + else { return } + self.cancelActiveCameraEncoding() + let sendableImage = OpenClawSendableCameraImage(value: image) + let fileName = "camera-\(UUID().uuidString.prefix(8)).jpg" + let viewModel = owner.viewModel + let session = owner.session + let generation = UUID() + self.cameraEncodingGeneration = generation + // Pin routing before JPEG encoding suspends so an external agent or + // session update cannot adopt this camera result under a new owner. + viewModel.beginAttachmentStaging() + self.cameraEncodingTask = Task { @MainActor in + defer { viewModel.endAttachmentStaging() } + let data = await Task.detached(priority: .userInitiated) { + sendableImage.value.jpegData(compressionQuality: 0.92) + }.value + guard !Task.isCancelled, + self.cameraEncodingGeneration == generation, + self.viewModel === viewModel, + viewModel.isCurrentSession(session), + self.isAttachmentInputEnabled, + let data + else { return } + await viewModel.addImageAttachment( + data: data, + fileName: fileName, + mimeType: "image/jpeg", + for: session) + } + } + #endif + + private func cancelActiveCameraEncoding() { + #if canImport(UIKit) + self.cameraEncodingGeneration = UUID() + self.cameraEncodingTask?.cancel() + self.cameraEncodingTask = nil + #endif + } + + private func stagePhotosPickerItems( + _ items: [PhotosPickerItem], + owner: OpenClawChatAttachmentCaptureOwner?) + { + guard self.isAttachmentInputEnabled else { + self.pickerItems = [] + return + } + guard let owner, + self.viewModel === owner.viewModel, + owner.viewModel.isCurrentSession(owner.session) + else { + self.pickerItems = [] + return + } + owner.viewModel.beginAttachmentStaging() + Task { @MainActor in + defer { owner.viewModel.endAttachmentStaging() } + await self.loadPhotosPickerItems(items, owner: owner) + } + } + + private func loadPhotosPickerItems( + _ items: [PhotosPickerItem], + owner: OpenClawChatAttachmentCaptureOwner) async + { guard self.isAttachmentInputEnabled else { self.pickerItems = [] return } for item in items { do { + guard self.viewModel === owner.viewModel, + owner.viewModel.isCurrentSession(owner.session) + else { break } guard let data = try await item.loadTransferable(type: Data.self) else { continue } let type = item.supportedContentTypes.first ?? .image let ext = type.preferredFilenameExtension ?? "jpg" let mime = type.preferredMIMEType ?? "image/jpeg" let name = "photo-\(UUID().uuidString.prefix(8)).\(ext)" - self.viewModel.addImageAttachment(data: data, fileName: name, mimeType: mime) + await owner.viewModel.addImageAttachment( + data: data, + fileName: name, + mimeType: mime, + for: owner.session) } catch { - self.viewModel.errorText = error.localizedDescription + guard self.viewModel === owner.viewModel, + owner.viewModel.isCurrentSession(owner.session) + else { break } + owner.viewModel.errorText = error.localizedDescription } } self.pickerItems = [] diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift index 0ab7af535c7a..e6f0a0b39c5b 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift @@ -49,6 +49,11 @@ func chatReaderScrollReleasesFollow(_ phase: ScrollPhase) -> Bool { } } +private enum ScrollFollowTarget: Equatable { + case latest + case user(UUID) +} + public struct OpenClawChatDisplayOptions: OptionSet, Sendable { public let rawValue: UInt8 @@ -117,14 +122,10 @@ public struct OpenClawChatView: View { private let emptyAssistantIntro: String? private let emptyAssistantPrompts: [StarterPrompt] private let talkControl: OpenClawChatTalkControl? + private let dictationControl: OpenClawChatDictationControl? private let voiceNoteControl: OpenClawChatVoiceNoteControl? private let speech: OpenClawChatSpeechController? - private enum ScrollFollowTarget: Equatable { - case latest - case user(UUID) - } - private enum Layout { #if os(macOS) static let outerPaddingHorizontal: CGFloat = 6 @@ -172,6 +173,7 @@ public struct OpenClawChatView: View { emptyAssistantIntro: String? = nil, emptyAssistantPrompts: [StarterPrompt] = [], talkControl: OpenClawChatTalkControl? = nil, + dictationControl: OpenClawChatDictationControl? = nil, voiceNoteControl: OpenClawChatVoiceNoteControl? = nil, speech: OpenClawChatSpeechController? = nil) { @@ -193,6 +195,7 @@ public struct OpenClawChatView: View { self.emptyAssistantIntro = emptyAssistantIntro self.emptyAssistantPrompts = emptyAssistantPrompts self.talkControl = talkControl + self.dictationControl = dictationControl self.voiceNoteControl = voiceNoteControl self.speech = speech } @@ -277,6 +280,7 @@ public struct OpenClawChatView: View { && !self.viewModel.isSendingAttachmentDraft, messagePlaceholder: self.messagePlaceholder, talkControl: self.talkControl, + dictationControl: self.dictationControl, voiceNoteControl: self.voiceNoteControl) } diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel+Attachments.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel+Attachments.swift index fd84f19c0a9f..4a386a12fe23 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel+Attachments.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel+Attachments.swift @@ -9,6 +9,91 @@ import UIKit #endif extension OpenClawChatViewModel { + public func addAttachments(urls: [URL]) { + self.beginAttachmentStaging() + Task { + defer { self.endAttachmentStaging() } + await self.loadAttachments(urls: urls) + } + } + + func addAttachments(urls: [URL], for session: SessionSnapshot) { + guard self.isCurrentSession(session) else { return } + self.beginAttachmentStaging() + Task { + defer { self.endAttachmentStaging() } + await self.loadAttachments(urls: urls, expectedSession: session) + } + } + + public func addImageAttachment(data: Data, fileName: String, mimeType: String) { + self.beginAttachmentStaging() + Task { + defer { self.endAttachmentStaging() } + await self.addImageAttachment(url: nil, data: data, fileName: fileName, mimeType: mimeType) + } + } + + func addImageAttachment( + data: Data, + fileName: String, + mimeType: String, + for session: SessionSnapshot) async + { + guard self.isCurrentSession(session) else { return } + self.beginAttachmentStaging() + defer { self.endAttachmentStaging() } + await self.addImageAttachment( + url: nil, + data: data, + fileName: fileName, + mimeType: mimeType, + expectedSession: session) + } + + public func removeAttachment(_ id: OpenClawPendingAttachment.ID) { + attachments.removeAll { $0.id == id } + applyDeferredExternalStateIfReady() + } + + /// True while replacing this model could move an attachment across chats. + public var isAttachmentOwnerPinned: Bool { + self.blocksAttachmentOwnerChange + } + + var blocksAttachmentOwnerChange: Bool { + attachmentOwnerIsActive() || + isSendingAttachmentDraft || + attachmentStagingCount > 0 || + !attachments.isEmpty + } + + func canCreateSessionForImmediateSwitch() -> Bool { + guard !self.blocksAttachmentOwnerChange else { + self.errorText = String( + localized: "Remove attachments or wait for delivery to resolve before starting a new chat.") + return false + } + return true + } + + /// Applies external owner changes once recording or staging releases them. + public func attachmentOwnerActivityChanged() { + applyDeferredExternalStateIfReady() + } + + /// File reads and image processing suspend before the attachment exists. + /// Keep their original chat owner pinned until staging succeeds or fails. + func beginAttachmentStaging() { + attachmentStagingCount += 1 + } + + func endAttachmentStaging() { + precondition(attachmentStagingCount > 0) + attachmentStagingCount -= 1 + applyDeferredExternalStateIfReady() + } + /// Stages a recorded m4a voice note and removes its temporary file. public func addVoiceNoteAttachment(fileURL: URL, durationSeconds: Double) async { self.beginAttachmentStaging() @@ -23,21 +108,21 @@ extension OpenClawChatViewModel { try Data(contentsOf: fileURL) }.value } catch { - self.errorText = String( + errorText = String( format: String(localized: "Could not attach voice note: %@"), error.localizedDescription) return } guard data.count <= Self.maxAttachmentBytes else { - self.errorText = String(localized: "Voice note exceeds the 5 MB attachment limit") + errorText = String(localized: "Voice note exceeds the 5 MB attachment limit") return } let normalizedDuration = durationSeconds.isFinite ? min(max(0, durationSeconds), OpenClawVoiceNoteRecorder.maximumDurationSeconds) : 0 - self.attachments.append( + attachments.append( OpenClawPendingAttachment( url: nil, data: data, @@ -47,17 +132,26 @@ extension OpenClawChatViewModel { durationSeconds: normalizedDuration)) } - func loadAttachments(urls: [URL]) async { + func loadAttachments(urls: [URL], expectedSession: SessionSnapshot? = nil) async { for url in urls { + guard self.ownsAttachmentSession(expectedSession) else { return } + let hasSecurityScope = url.startAccessingSecurityScopedResource() + defer { + if hasSecurityScope { + url.stopAccessingSecurityScopedResource() + } + } do { let data = try await Task.detached { try Data(contentsOf: url) }.value await self.addImageAttachment( url: url, data: data, fileName: url.lastPathComponent, - mimeType: Self.mimeType(for: url) ?? "application/octet-stream") + mimeType: Self.mimeType(for: url) ?? "application/octet-stream", + expectedSession: expectedSession) } catch { - await MainActor.run { self.errorText = error.localizedDescription } + guard self.ownsAttachmentSession(expectedSession) else { return } + self.errorText = error.localizedDescription } } } @@ -68,7 +162,14 @@ extension OpenClawChatViewModel { return (UTType(filenameExtension: ext) ?? .data).preferredMIMEType } - func addImageAttachment(url: URL?, data: Data, fileName: String, mimeType: String) async { + func addImageAttachment( + url: URL?, + data: Data, + fileName: String, + mimeType: String, + expectedSession: SessionSnapshot? = nil) async + { + guard self.ownsAttachmentSession(expectedSession) else { return } let uti: UTType = { if let url { return UTType(filenameExtension: url.pathExtension) ?? .data @@ -76,7 +177,7 @@ extension OpenClawChatViewModel { return UTType(mimeType: mimeType) ?? .data }() guard uti.conforms(to: .image) else { - self.errorText = String(localized: "Only image attachments are supported right now") + errorText = String(localized: "Only image attachments are supported right now") return } @@ -86,15 +187,19 @@ extension OpenClawChatViewModel { try ChatImageProcessor.processForUpload(data: data) }.value } catch { - self.errorText = String( + guard self.ownsAttachmentSession(expectedSession) else { return } + errorText = String( format: String(localized: "Could not process %1$@: %2$@"), fileName, error.localizedDescription) return } + // Image processing runs off actor. Revalidate the draft owner before + // publishing either the attachment or any session-scoped error state. + guard self.ownsAttachmentSession(expectedSession) else { return } if processed.count > Self.maxAttachmentBytes { - self.errorText = String( + errorText = String( format: String(localized: "Attachment %@ exceeds 5 MB limit after resizing"), fileName) return @@ -106,7 +211,7 @@ extension OpenClawChatViewModel { }() let preview = Self.previewImage(data: processed) - self.attachments.append( + attachments.append( OpenClawPendingAttachment( url: url, data: processed, @@ -115,6 +220,10 @@ extension OpenClawChatViewModel { preview: preview)) } + private func ownsAttachmentSession(_ expectedSession: SessionSnapshot?) -> Bool { + expectedSession.map(self.isCurrentSession) ?? true + } + static func previewImage(data: Data) -> OpenClawPlatformImage? { #if canImport(AppKit) NSImage(data: data) diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel+Sending.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel+Sending.swift index 673c78c612b0..c5122da8c1e5 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel+Sending.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel+Sending.swift @@ -2,26 +2,49 @@ import Foundation import OpenClawKit import OSLog +struct SlashFilterCache { + let query: String + let filter: OpenClawChatCommandFilter + let result: [OpenClawChatCommandChoice] +} + private let chatSendingLogger = Logger(subsystem: "ai.openclaw", category: "OpenClawChatUI") extension OpenClawChatViewModel { + public var canSend: Bool { + !isSubmittingDraft && + !isSending && + self.attachmentStagingCount == 0 && + !self.hasBlockingRunActivity && + self.hasDraftToSend + } + + public var hasDraftToSend: Bool { + let trimmed = input.trimmingCharacters(in: .whitespacesAndNewlines) + return !trimmed.isEmpty || !attachments.isEmpty + } + + var hasBlockingRunActivity: Bool { + pendingRunCount > 0 || hasActiveSessionRunWithoutChatSnapshot + } + public func send() { - self.logDiagnostic( - "chat.ui send invoked sessionKey=\(self.sessionKey) " - + "inputLen=\(self.input.count) attachments=\(self.attachments.count) " - + "pending=\(self.pendingRunCount) sending=\(self.isSending) " - + "health=\(self.healthOK)") + logDiagnostic( + "chat.ui send invoked sessionKey=\(sessionKey) " + + "inputLen=\(input.count) attachments=\(attachments.count) " + + "pending=\(pendingRunCount) sending=\(isSending) " + + "health=\(healthOK)") Task { await self.performSend() } } public func loadSlashCommandsIfNeeded() { - guard self.transport.supportsSlashCommandCatalog else { return } - guard !self.hasLoadedSlashCommands, !self.isLoadingSlashCommands else { return } + guard transport.supportsSlashCommandCatalog else { return } + guard !hasLoadedSlashCommands, !isLoadingSlashCommands else { return } Task { await self.loadSlashCommands(force: false) } } public func refreshSlashCommands() { - guard self.transport.supportsSlashCommandCatalog else { return } + guard transport.supportsSlashCommandCatalog else { return } Task { await self.loadSlashCommands(force: true) } } @@ -32,49 +55,49 @@ extension OpenClawChatViewModel { if let cache = slashFilterCache, cache.query == query, cache.filter == filter { return cache.result } - let result = Self.filteredSlashCommands(self.slashCommands, query: query, filter: filter) - self.slashFilterCache = SlashFilterCache(query: query, filter: filter, result: result) + let result = Self.filteredSlashCommands(slashCommands, query: query, filter: filter) + slashFilterCache = SlashFilterCache(query: query, filter: filter, result: result) return result } public func applySlashCommandSelection(_ command: OpenClawChatCommandChoice) { let invocation = command.preferredInvocation.trimmingCharacters(in: .whitespacesAndNewlines) guard !invocation.isEmpty else { return } - self.input = command.acceptsArgs ? "\(invocation) " : invocation - self.errorText = nil + input = command.acceptsArgs ? "\(invocation) " : invocation + errorText = nil } private static let resetTriggers: Set = ["/reset", "/clear"] private static let compactTriggers: Set = ["/compact"] private func loadSlashCommands(force: Bool) async { - guard self.transport.supportsSlashCommandCatalog else { return } - guard force || !self.hasLoadedSlashCommands else { return } - guard !self.isLoadingSlashCommands else { return } - let sessionSnapshot = self.currentSessionSnapshot() - self.isLoadingSlashCommands = true + guard transport.supportsSlashCommandCatalog else { return } + guard force || !hasLoadedSlashCommands else { return } + guard !isLoadingSlashCommands else { return } + let sessionSnapshot = currentSessionSnapshot() + isLoadingSlashCommands = true defer { self.isLoadingSlashCommands = false } do { let commands = try await transport.listCommands(sessionKey: sessionSnapshot.key) - guard self.isCurrentSession(sessionSnapshot) else { return } - self.slashCommands = commands - self.slashFilterCache = nil - self.slashCommandsErrorText = nil - self.hasLoadedSlashCommands = true + guard isCurrentSession(sessionSnapshot) else { return } + slashCommands = commands + slashFilterCache = nil + slashCommandsErrorText = nil + hasLoadedSlashCommands = true } catch { - guard self.isCurrentSession(sessionSnapshot) else { return } - self.slashCommandsErrorText = error.localizedDescription + guard isCurrentSession(sessionSnapshot) else { return } + slashCommandsErrorText = error.localizedDescription } } private func waitForSlashCommandLoadIfNeeded() async { - guard self.transport.supportsSlashCommandCatalog else { return } - if !self.hasLoadedSlashCommands, !self.isLoadingSlashCommands { + guard transport.supportsSlashCommandCatalog else { return } + if !hasLoadedSlashCommands, !isLoadingSlashCommands { await self.loadSlashCommands(force: false) return } - while self.isLoadingSlashCommands { + while isLoadingSlashCommands { do { try await Task.sleep(nanoseconds: 50_000_000) } catch { @@ -88,27 +111,27 @@ extension OpenClawChatViewModel { return true } guard !slashName.isEmpty else { - self.errorText = "Choose a command." + errorText = "Choose a command." return false } await self.waitForSlashCommandLoadIfNeeded() - if self.hasLoadedSlashCommands, - Self.isKnownSlashCommandText(trimmed, commands: self.slashCommands), + if hasLoadedSlashCommands, + Self.isKnownSlashCommandText(trimmed, commands: slashCommands), hasAttachments { - self.errorText = "Commands cannot be sent with attachments." + errorText = "Commands cannot be sent with attachments." return false } return true } func resetSlashCommandCatalog() { - self.slashCommands = [] - self.slashFilterCache = nil - self.slashCommandsErrorText = nil - self.hasLoadedSlashCommands = false + slashCommands = [] + slashFilterCache = nil + slashCommandsErrorText = nil + hasLoadedSlashCommands = false } private static func slashCommandName(from text: String) -> String? { @@ -249,18 +272,24 @@ extension OpenClawChatViewModel { private func handleLocalSlashCommandIfNeeded(_ command: String, draftInput: String) async -> Bool { if command == "/new" { - if self.input == draftInput { self.input = "" } - await self.performStartNewSession(worktree: false) + if input == draftInput { + input = "" + } + await performStartNewSession(worktree: false) return true } if Self.resetTriggers.contains(command) { - if self.input == draftInput { self.input = "" } - await self.performReset() + if input == draftInput { + input = "" + } + await performReset() return true } if Self.compactTriggers.contains(command) { - if self.input == draftInput { self.input = "" } - await self.performCompact() + if input == draftInput { + input = "" + } + await performCompact() return true } return false @@ -275,9 +304,9 @@ extension OpenClawChatViewModel { // after its transport disconnects without a health event. performSend // owns the send gate across this await. await pollHealthIfNeeded(force: true, sessionSnapshot: session) - guard self.isCurrentSession(session) else { return false } - guard self.healthOK else { - self.errorText = "Connect to the gateway to run this command." + guard isCurrentSession(session) else { return false } + guard healthOK else { + errorText = "Connect to the gateway to run this command." return false } return true @@ -320,20 +349,20 @@ extension OpenClawChatViewModel { } private func performSend() async { - guard let draft = self.captureSendDraft() else { return } + guard let draft = captureSendDraft() else { return } // Own every asynchronous validation/probe below. Slash catalog lookup // can suspend, so taking this gate later permits duplicate enqueues. // It also makes the captured reply selection single-submission; exact // target identity keeps a later re-selection safe from completion. // Keep it separate from isSending: local /compact checks that flag. - self.isSubmittingDraft = true + isSubmittingDraft = true defer { self.isSubmittingDraft = false } guard await self.validateSendDraft(draft) else { return } - self.isSending = true - self.isSendingAttachmentDraft = !draft.attachments.isEmpty + isSending = true + isSendingAttachmentDraft = !draft.attachments.isEmpty defer { self.isSendingAttachmentDraft = false self.isSending = false @@ -346,37 +375,43 @@ extension OpenClawChatViewModel { } private func captureSendDraft() -> SendDraft? { - guard !self.isSubmittingDraft, !self.isSending else { - self.logDiagnostic("chat.ui send ignored reason=sending sessionKey=\(self.sessionKey)") + guard !isSubmittingDraft, !isSending else { + logDiagnostic("chat.ui send ignored reason=sending sessionKey=\(sessionKey)") + return nil + } + guard self.attachmentStagingCount == 0 else { + // File reads and image processing suspend before publishing the + // attachment. Do not let a programmatic send overtake that owner. + logDiagnostic("chat.ui send ignored reason=attachment-staging sessionKey=\(sessionKey)") return nil } guard !self.hasBlockingRunActivity else { - self.logDiagnostic( - "chat.ui send ignored reason=pending sessionKey=\(self.sessionKey) " - + "pending=\(self.pendingRunCount) " - + "activeWithoutSnapshot=\(self.hasActiveSessionRunWithoutChatSnapshot)") + logDiagnostic( + "chat.ui send ignored reason=pending sessionKey=\(sessionKey) " + + "pending=\(pendingRunCount) " + + "activeWithoutSnapshot=\(hasActiveSessionRunWithoutChatSnapshot)") return nil } let input = self.input let attachments = self.attachments let trimmed = input.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty || !attachments.isEmpty else { - self.logDiagnostic("chat.ui send ignored reason=empty sessionKey=\(self.sessionKey)") + logDiagnostic("chat.ui send ignored reason=empty sessionKey=\(sessionKey)") return nil } return SendDraft( input: input, attachments: attachments, trimmed: trimmed, - session: self.currentSessionSnapshot(), - replyTarget: Self.isSlashCommandDraft(trimmed) ? nil : self.replyTarget, - composerRevision: self.composerRevision(for: self.sessionKey)) + session: currentSessionSnapshot(), + replyTarget: Self.isSlashCommandDraft(trimmed) ? nil : replyTarget, + composerRevision: composerRevision(for: sessionKey)) } private func validateSendDraft(_ draft: SendDraft) async -> Bool { let command = draft.trimmed.lowercased() if Self.isLiveOnlyLocalSlashCommand(command) { - let canRunCommand = await self.prepareLiveOnlyLocalSlashCommand(session: draft.session) + let canRunCommand = await prepareLiveOnlyLocalSlashCommand(session: draft.session) guard canRunCommand else { return false } } if await self.handleLocalSlashCommandIfNeeded(command, draftInput: draft.input) { @@ -393,15 +428,15 @@ extension OpenClawChatViewModel { private func prepareLiveRoute(for draft: SendDraft) async -> Bool { let sessionKey = draft.session.key - if !self.healthOK { - await self.pollHealthIfNeeded(force: true, sessionSnapshot: draft.session) - guard self.isCurrentSession(draft.session) else { return false } + if !healthOK { + await pollHealthIfNeeded(force: true, sessionSnapshot: draft.session) + guard isCurrentSession(draft.session) else { return false } // Offline capture: queue the full draft durably instead of // dropping user text or attachment bytes. - if !self.healthOK, self.outbox != nil { - self.logDiagnostic( + if !healthOK, outbox != nil { + logDiagnostic( "chat.ui send queued offline sessionKey=\(sessionKey) inputLen=\(draft.trimmed.count)") - let accepted = await self.enqueueOutboxCommand( + let accepted = await enqueueOutboxCommand( text: draft.outgoingMessageText, draftInput: draft.input, draftRevision: draft.composerRevision, @@ -414,9 +449,9 @@ extension OpenClawChatViewModel { } } - let mustPreserveOutboxOrder = !self.hasRestoredOutboxMessages || - self.outboxStatesByMessageID.values.contains(where: { !$0.isFailed }) - let attachmentDecision = await self.attachmentPersistenceDecision( + let mustPreserveOutboxOrder = !hasRestoredOutboxMessages || + outboxStatesByMessageID.values.contains(where: { !$0.isFailed }) + let attachmentDecision = await attachmentPersistenceDecision( draft, mustPreserveOutboxOrder: mustPreserveOutboxOrder) let shouldPersistAttachmentDraft: Bool @@ -440,12 +475,12 @@ extension OpenClawChatViewModel { // path so a crash cannot erase their only remaining bytes. Deliberately // session-scoped: other sessions are separate conversations with no // ordering contract. - if self.outbox != nil, + if outbox != nil, shouldPersistAttachmentDraft || mustPreserveOutboxOrder { - self.logDiagnostic( + logDiagnostic( "chat.ui send routed behind outbox sessionKey=\(sessionKey) inputLen=\(draft.trimmed.count)") - let accepted = await self.enqueueOutboxCommand( + let accepted = await enqueueOutboxCommand( text: draft.outgoingMessageText, draftInput: draft.input, draftRevision: draft.composerRevision, @@ -464,26 +499,26 @@ extension OpenClawChatViewModel { mustPreserveOutboxOrder: Bool) async -> AttachmentPersistenceDecision { guard !draft.attachments.isEmpty, - self.healthOK, - self.outbox != nil + healthOK, + outbox != nil else { return draft.attachments.isEmpty ? .liveOnly : .persistIfAvailable } - let routeResult = await self.transport.acquireOutboxRouteLease() - guard self.isCurrentSession(draft.session) else { return .stop } + let routeResult = await transport.acquireOutboxRouteLease() + guard isCurrentSession(draft.session) else { return .stop } guard case let .unavailable(reason) = routeResult, reason == OpenClawChatTransportUpgradeMessage.routingContract else { return .persistIfAvailable } - guard self.hasRestoredOutboxMessages else { - self.errorText = "Restoring queued messages. Try again in a moment." + guard hasRestoredOutboxMessages else { + errorText = "Restoring queued messages. Try again in a moment." return .stop } guard !mustPreserveOutboxOrder else { // A legacy gateway cannot drain the existing durable rows, so keep // this new attachment in the composer behind them. - self.errorText = reason + errorText = reason return .stop } // Older healthy gateways can send attachments live but cannot safely @@ -492,16 +527,16 @@ extension OpenClawChatViewModel { } private func beginLiveSend(_ draft: SendDraft) -> LiveSendAttempt { - self.errorText = nil + errorText = nil let runId = UUID().uuidString - let storedThinkingLevel = self.preferredThinkingLevel - self.pendingRuns.insert(runId) - self.logDiagnostic( + let storedThinkingLevel = preferredThinkingLevel + pendingRuns.insert(runId) + logDiagnostic( "chat.ui send queued sessionKey=\(draft.session.key) " - + "localRunId=\(runId) pending=\(self.pendingRunCount)") - self.pendingToolCallsById = [:] - self.updateStreamingAssistantText(nil) - self.clearPlan() + + "localRunId=\(runId) pending=\(pendingRunCount)") + pendingToolCallsById = [:] + updateStreamingAssistantText(nil) + clearPlan() // Production attachment sends enter the durable outbox above. Fixture, // preview, and embedded transports may intentionally have no outbox; @@ -519,22 +554,22 @@ extension OpenClawChatViewModel { encodedAttachments: encodedAttachments) let userMessageTimestamp = Date().timeIntervalSince1970 * 1000 let userMessageID = UUID() - self.appendMessage( + appendMessage( OpenClawChatMessage( id: userMessageID, role: "user", content: userContent, timestamp: userMessageTimestamp, idempotencyKey: "\(runId):user")) - self.pendingLocalUserEchoMessageIDsByRunID[runId] = userMessageID - self.runMessageScopesByRunID[runId] = self.currentRunMessageScope() + pendingLocalUserEchoMessageIDsByRunID[runId] = userMessageID + runMessageScopesByRunID[runId] = currentRunMessageScope() // Clear input immediately for responsive UX (before network await). - if self.input == draft.input { - self.input = "" + if input == draft.input { + input = "" } let sentAttachmentIDs = Set(draft.attachments.map(\.id)) - self.attachments.removeAll { sentAttachmentIDs.contains($0.id) } + attachments.removeAll { sentAttachmentIDs.contains($0.id) } return LiveSendAttempt( draft: draft, @@ -584,13 +619,13 @@ extension OpenClawChatViewModel { private func deliverLiveSend(_ attempt: LiveSendAttempt) async { let sessionKey = attempt.draft.session.key do { - await self.waitForPendingSessionSettings(in: sessionKey) - guard self.isCurrentSession(attempt.draft.session) else { return } - self.logDiagnostic( + await waitForPendingSessionSettings(in: sessionKey) + guard isCurrentSession(attempt.draft.session) else { return } + logDiagnostic( "chat.ui transport send start sessionKey=\(sessionKey) " + "localRunId=\(attempt.runId)") - let thinkingLevel = self.effectiveThinkingLevelForSend(attempt.storedThinkingLevel) - let response = try await self.transport.sendMessage( + let thinkingLevel = effectiveThinkingLevelForSend(attempt.storedThinkingLevel) + let response = try await transport.sendMessage( sessionKey: sessionKey, agentID: attempt.draft.session.deliveryAgentID, expectedSessionRoutingContract: attempt.draft.session.sessionRoutingContract, @@ -598,7 +633,7 @@ extension OpenClawChatViewModel { thinking: thinkingLevel, idempotencyKey: attempt.runId, attachments: attempt.encodedAttachments) - guard self.isCurrentSession(attempt.draft.session) else { return } + guard isCurrentSession(attempt.draft.session) else { return } await self.handleLiveSendResponse(response, attempt: attempt) } catch { await self.handleLiveSendFailure(error, attempt: attempt) @@ -610,11 +645,11 @@ extension OpenClawChatViewModel { attempt: LiveSendAttempt) async { let sessionKey = attempt.draft.session.key - self.logDiagnostic( + logDiagnostic( "chat.ui transport send accepted sessionKey=\(sessionKey) " + "localRunId=\(attempt.runId) remoteRunId=\(response.runId)") if response.status != "error", response.status != "timeout" { - self.haptics.perform(.messageSent) + haptics.perform(.messageSent) self.finishAcceptedComposerSend(attempt.draft) } let reusedRunAlreadyFinal = response.runId == attempt.runId @@ -622,30 +657,30 @@ extension OpenClawChatViewModel { : self.adoptRemoteRunID(response.runId, replacing: attempt.runId) if response.status == "ok" { - let historyContext = self.beginHistoryRequest(for: attempt.draft.session) - await self.refreshHistoryAfterRun(historyRequest: historyContext) - guard self.isCurrentSession(attempt.draft.session) else { return } - self.finishPendingRunAfterTerminalOkSendAck(response) + let historyContext = beginHistoryRequest(for: attempt.draft.session) + await refreshHistoryAfterRun(historyRequest: historyContext) + guard isCurrentSession(attempt.draft.session) else { return } + finishPendingRunAfterTerminalOkSendAck(response) return } - guard !self.finishPendingRunIfTerminalSendAck(response), + guard !finishPendingRunIfTerminalSendAck(response), !reusedRunAlreadyFinal else { return } - let historyContext = self.beginHistoryRequest(for: attempt.draft.session) - let refresh = await self.refreshHistoryAfterRun(historyRequest: historyContext) - guard self.isCurrentSession(attempt.draft.session) else { return } + let historyContext = beginHistoryRequest(for: attempt.draft.session) + let refresh = await refreshHistoryAfterRun(historyRequest: historyContext) + guard isCurrentSession(attempt.draft.session) else { return } let hasInFlightRunSnapshot = refresh.applied && refresh.runSnapshotApplied && refresh.hasInFlightRun if hasInFlightRunSnapshot || - !self.clearPendingRunIfAssistantMessagePresent( + !clearPendingRunIfAssistantMessagePresent( runId: response.runId, after: attempt.userMessageTimestamp) { - self.armPendingRunOwner( + armPendingRunOwner( runId: response.runId, sessionSnapshot: attempt.draft.session, userMessageTimestamp: attempt.userMessageTimestamp) @@ -653,27 +688,27 @@ extension OpenClawChatViewModel { } private func adoptRemoteRunID(_ remoteRunId: String, replacing localRunId: String) -> Bool { - let pendingUserMessageID = self.pendingLocalUserEchoMessageIDsByRunID.removeValue(forKey: localRunId) - let localRunScope = self.runMessageScopesByRunID.removeValue(forKey: localRunId) - self.clearPendingRun(localRunId) - self.pendingRuns.insert(remoteRunId) + let pendingUserMessageID = pendingLocalUserEchoMessageIDsByRunID.removeValue(forKey: localRunId) + let localRunScope = runMessageScopesByRunID.removeValue(forKey: localRunId) + clearPendingRun(localRunId) + pendingRuns.insert(remoteRunId) // The gateway can reuse an identical active run without writing this // second turn. Move the optimistic row onto that durable identity, // collapsing it if the canonical row is already here. - let rekeyedUserEcho = self.rekeyLocalUserEcho( + let rekeyedUserEcho = rekeyLocalUserEcho( messageID: pendingUserMessageID, runId: remoteRunId) - self.pendingLocalUserEchoMessageIDsByRunID[remoteRunId] = rekeyedUserEcho?.pendingMessageID - let remoteRunScope = rekeyedUserEcho?.scope ?? localRunScope ?? self.currentRunMessageScope() - self.runMessageScopesByRunID[remoteRunId] = remoteRunScope - self.rescopeProvisionalFinalMessages(runId: remoteRunId, scope: remoteRunScope) - let reusedRunAlreadyFinal = self.hasRecordedFinalMessage(runId: remoteRunId) + pendingLocalUserEchoMessageIDsByRunID[remoteRunId] = rekeyedUserEcho?.pendingMessageID + let remoteRunScope = rekeyedUserEcho?.scope ?? localRunScope ?? currentRunMessageScope() + runMessageScopesByRunID[remoteRunId] = remoteRunScope + rescopeProvisionalFinalMessages(runId: remoteRunId, scope: remoteRunScope) + let reusedRunAlreadyFinal = hasRecordedFinalMessage(runId: remoteRunId) if reusedRunAlreadyFinal { - self.clearPendingRun(remoteRunId, hapticEvent: .runCompleted) - self.pendingToolCallsById = [:] - self.updateStreamingAssistantText(nil) + clearPendingRun(remoteRunId, hapticEvent: .runCompleted) + pendingToolCallsById = [:] + updateStreamingAssistantText(nil) } else { - self.armPendingRunOwner( + armPendingRunOwner( runId: remoteRunId, sessionSnapshot: remoteRunScope.session, userMessageTimestamp: remoteRunScope.latestUserTurn?.timestamp) @@ -682,57 +717,57 @@ extension OpenClawChatViewModel { } private func handleLiveSendFailure(_ error: Error, attempt: LiveSendAttempt) async { - guard self.isCurrentSession(attempt.draft.session) else { return } + guard isCurrentSession(attempt.draft.session) else { return } if attempt.encodedAttachments.isEmpty, !(error is GatewayResponseError) { - self.runMessageScopesByRunID.removeValue(forKey: attempt.runId) - self.clearPendingRun(attempt.runId) + runMessageScopesByRunID.removeValue(forKey: attempt.runId) + clearPendingRun(attempt.runId) let deliveryIsAmbiguous = !(error is OpenClawChatTransportSendError) - let preserved = await self.preserveFailedLiveSend( + let preserved = await preserveFailedLiveSend( runId: attempt.runId, text: attempt.draft.outgoingMessageText, - thinking: self.effectiveThinkingLevelForSend(attempt.storedThinkingLevel), + thinking: effectiveThinkingLevelForSend(attempt.storedThinkingLevel), messageID: attempt.userMessageID, session: attempt.draft.session, deliveryIsAmbiguous: deliveryIsAmbiguous) if preserved { self.finishAcceptedComposerSend(attempt.draft) - self.applyTransportHealth(false) + applyTransportHealth(false) let outcome = deliveryIsAmbiguous ? "delivery unconfirmed" : "queued after route change" - self.logDiagnostic( + logDiagnostic( "chat.ui send \(outcome) sessionKey=\(attempt.draft.session.key) " + "localRunId=\(attempt.runId) error=\(error.localizedDescription)") return } - guard self.isCurrentSession(attempt.draft.session) else { return } + guard isCurrentSession(attempt.draft.session) else { return } // Refused persistence (queue full / broken store): restore the // draft so the text is not lost with the failed bubble. - if self.input.isEmpty { - self.input = attempt.draft.input + if input.isEmpty { + input = attempt.draft.input } } self.restoreDraftAfterLiveSendFailure(attempt) - self.removePendingLocalUserEcho(for: attempt.runId) - self.runMessageScopesByRunID.removeValue(forKey: attempt.runId) - self.errorText = error.localizedDescription - self.clearPendingRun(attempt.runId, hapticEvent: .runFailed) - self.logDiagnostic( + removePendingLocalUserEcho(for: attempt.runId) + runMessageScopesByRunID.removeValue(forKey: attempt.runId) + errorText = error.localizedDescription + clearPendingRun(attempt.runId, hapticEvent: .runFailed) + logDiagnostic( "chat.ui send failed sessionKey=\(attempt.draft.session.key) " + "localRunId=\(attempt.runId) error=\(error.localizedDescription)") chatSendingLogger.error("chat transport send failed \(error.localizedDescription, privacy: .public)") } private func restoreDraftAfterLiveSendFailure(_ attempt: LiveSendAttempt) { - if attempt.encodedAttachments.isEmpty, self.input.isEmpty { - self.input = attempt.draft.input + if attempt.encodedAttachments.isEmpty, input.isEmpty { + input = attempt.draft.input } else if !attempt.encodedAttachments.isEmpty { - if self.input.isEmpty { - self.input = attempt.draft.input + if input.isEmpty { + input = attempt.draft.input } - let currentAttachmentIDs = Set(self.attachments.map(\.id)) + let currentAttachmentIDs = Set(attachments.map(\.id)) let removedDraftAttachments = attempt.draft.attachments.filter { !currentAttachmentIDs.contains($0.id) } - self.attachments.insert(contentsOf: removedDraftAttachments, at: 0) + attachments.insert(contentsOf: removedDraftAttachments, at: 0) } } diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel+ToolActivity.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel+ToolActivity.swift new file mode 100644 index 000000000000..3c2096a14c34 --- /dev/null +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel+ToolActivity.swift @@ -0,0 +1,25 @@ +import Foundation + +public typealias OpenClawChatToolActivityHandler = @MainActor @Sendable ( + _ id: String, + _ name: String, + _ isActive: Bool, + _ sessionKey: String) -> Void + +extension OpenClawChatViewModel { + public func endPendingToolActivities() { + self.pendingToolCallsById = [:] + } + + func reportToolActivityChanges( + from previous: [String: OpenClawChatPendingToolCall], + to current: [String: OpenClawChatPendingToolCall]) + { + for (id, call) in previous where current[id] == nil { + self.onToolActivity?(id, call.name, false, self.sessionKey) + } + for (id, call) in current where previous[id] == nil { + self.onToolActivity?(id, call.name, true, self.sessionKey) + } + } +} diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel+TranscriptCache.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel+TranscriptCache.swift index 66642c0ff926..94160ade6a35 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel+TranscriptCache.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel+TranscriptCache.swift @@ -5,7 +5,7 @@ import Foundation // of truth and replace cached rows wholesale. extension OpenClawChatViewModel { - struct SessionSnapshot { + struct SessionSnapshot: Equatable { var key: String var generation: UInt64 var agentID: String? diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift index b04dabd87158..ee4cde5ca4a9 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift @@ -58,12 +58,6 @@ public final class OpenClawChatViewModel { @ObservationIgnored var slashFilterCache: SlashFilterCache? - struct SlashFilterCache { - let query: String - let filter: OpenClawChatCommandFilter - let result: [OpenClawChatCommandChoice] - } - private struct DeferredDeliveryIdentity { let activeAgentID: String? let sessionRoutingContract: String? @@ -76,7 +70,7 @@ public final class OpenClawChatViewModel { private var deferredExternalSessionKey: String? private var deferredDeliveryIdentity: DeferredDeliveryIdentity? var isSubmittingDraft = false - private var attachmentStagingCount = 0 + var attachmentStagingCount = 0 public private(set) var isAborting = false public var errorText: String? public var attachments: [OpenClawPendingAttachment] = [] @@ -185,7 +179,8 @@ public final class OpenClawChatViewModel { let onVerboseLevelChanged: (@MainActor @Sendable (String) -> Void)? let onVerbosePreferenceChanged: (@MainActor @Sendable (String?) -> Void)? private let diagnosticsLog: (@MainActor @Sendable (String) -> Void)? - private let attachmentOwnerIsActive: @MainActor () -> Bool + let onToolActivity: OpenClawChatToolActivityHandler? + let attachmentOwnerIsActive: @MainActor () -> Bool @ObservationIgnored private nonisolated(unsafe) var eventTask: Task? @@ -400,6 +395,7 @@ public final class OpenClawChatViewModel { var pendingToolCallsById: [String: OpenClawChatPendingToolCall] = [:] { didSet { guard self.pendingToolCallsById != oldValue else { return } + reportToolActivityChanges(from: oldValue, to: self.pendingToolCallsById) self.pendingToolCalls = self.pendingToolCallsById.values .sorted { ($0.startedAt ?? 0) < ($1.startedAt ?? 0) } markTimelineChanged() @@ -422,6 +418,7 @@ public final class OpenClawChatViewModel { initialVerboseLevel: String? = nil, onSessionChanged: (@MainActor (String) -> Void)? = nil, onThinkingLevelChanged: (@MainActor @Sendable (String) -> Void)? = nil, + onToolActivity: OpenClawChatToolActivityHandler? = nil, onThinkingPreferenceChanged: (@MainActor @Sendable (String?) -> Void)? = nil, onVerboseLevelChanged: (@MainActor @Sendable (String) -> Void)? = nil, onVerbosePreferenceChanged: (@MainActor @Sendable (String?) -> Void)? = nil, @@ -464,6 +461,7 @@ public final class OpenClawChatViewModel { self.emittedVerbosePreference = initialVerbosePreference self.onSessionChanged = onSessionChanged self.onThinkingLevelChanged = onThinkingLevelChanged + self.onToolActivity = onToolActivity self.onThinkingPreferenceChanged = onThinkingPreferenceChanged self.onVerboseLevelChanged = onVerboseLevelChanged self.onVerbosePreferenceChanged = onVerbosePreferenceChanged @@ -494,6 +492,7 @@ public final class OpenClawChatViewModel { } isolated deinit { + self.reportToolActivityChanges(from: self.pendingToolCallsById, to: [:]) self.eventTask?.cancel() self.bootstrapTask?.cancel() self.outboxRetryTask?.cancel() @@ -615,12 +614,12 @@ public final class OpenClawChatViewModel { let agentChanged = self.activeAgentId != nextAgentId let contractChanged = self.sessionRoutingContract != nextContract guard agentChanged || contractChanged else { - if self.blocksAttachmentOwnerChange { + if blocksAttachmentOwnerChange { self.deferredDeliveryIdentity = nil } return } - if self.blocksAttachmentOwnerChange { + if blocksAttachmentOwnerChange { self.deferredDeliveryIdentity = DeferredDeliveryIdentity( activeAgentID: nextAgentId, sessionRoutingContract: nextContract) @@ -723,79 +722,6 @@ public final class OpenClawChatViewModel { OpenClawChatThinkingLevelOption(id: "medium", label: "medium"), OpenClawChatThinkingLevelOption(id: "high", label: "high"), ] - - public func addAttachments(urls: [URL]) { - self.beginAttachmentStaging() - Task { - defer { self.endAttachmentStaging() } - await self.loadAttachments(urls: urls) - } - } - - public func addImageAttachment(data: Data, fileName: String, mimeType: String) { - self.beginAttachmentStaging() - Task { - defer { self.endAttachmentStaging() } - await self.addImageAttachment(url: nil, data: data, fileName: fileName, mimeType: mimeType) - } - } - - public func removeAttachment(_ id: OpenClawPendingAttachment.ID) { - self.attachments.removeAll { $0.id == id } - applyDeferredExternalStateIfReady() - } - - public var canSend: Bool { - !self.isSubmittingDraft && !self.isSending && !self.hasBlockingRunActivity && self.hasDraftToSend - } - - public var hasDraftToSend: Bool { - let trimmed = self.input.trimmingCharacters(in: .whitespacesAndNewlines) - return !trimmed.isEmpty || !self.attachments.isEmpty - } - - /// True while replacing this model could move an attachment across chats. - public var isAttachmentOwnerPinned: Bool { - self.blocksAttachmentOwnerChange - } - - /// Module-internal: the session-actions extension guards sessions.create with it. - var blocksAttachmentOwnerChange: Bool { - self.attachmentOwnerIsActive() || - self.isSendingAttachmentDraft || - self.attachmentStagingCount > 0 || - !self.attachments.isEmpty - } - - func canCreateSessionForImmediateSwitch() -> Bool { - guard !self.blocksAttachmentOwnerChange else { - self.errorText = String( - localized: "Remove attachments or wait for delivery to resolve before starting a new chat.") - return false - } - return true - } - - var hasBlockingRunActivity: Bool { - self.pendingRunCount > 0 || self.hasActiveSessionRunWithoutChatSnapshot - } - - /// Applies external owner changes once recording or staging releases them. - public func attachmentOwnerActivityChanged() { - applyDeferredExternalStateIfReady() - } - - /// File reads and image processing suspend before the attachment exists. - /// Keep their original chat owner pinned until staging succeeds or fails. - func beginAttachmentStaging() { - self.attachmentStagingCount += 1 - } - - func endAttachmentStaging() { - precondition(self.attachmentStagingCount > 0) - self.attachmentStagingCount -= 1 - applyDeferredExternalStateIfReady() - } } extension OpenClawChatViewModel { @@ -1147,7 +1073,7 @@ extension OpenClawChatViewModel { } return } - if self.blocksAttachmentOwnerChange { + if blocksAttachmentOwnerChange { switch intent { case .externalSync: self.deferredExternalSessionKey = next @@ -1160,17 +1086,17 @@ extension OpenClawChatViewModel { self.deferredExternalSessionKey = nil self.prepareComposerForSessionSwitch(to: next) self.advanceSessionGeneration() + self.clearSessionOwnedState() self.sessionKey = next self.restoreComposerAfterSessionSwitch() if intent == .userInitiated { self.onSessionChanged?(next) } - self.clearSessionOwnedState() self.startBootstrap(sessionKey: next) } func applyDeferredExternalStateIfReady() { - guard !self.blocksAttachmentOwnerChange else { return } + guard !blocksAttachmentOwnerChange else { return } if let identity = deferredDeliveryIdentity { self.deferredDeliveryIdentity = nil self.syncDeliveryIdentity( @@ -1188,10 +1114,10 @@ extension OpenClawChatViewModel { func adoptCreatedSession(_ next: String) { self.prepareComposerForSessionSwitch(to: next) self.advanceSessionGeneration() + self.clearSessionOwnedState() self.sessionKey = next self.restoreComposerAfterSessionSwitch() self.onSessionChanged?(next) - self.clearSessionOwnedState() self.errorText = nil self.startBootstrap() } @@ -1236,7 +1162,7 @@ extension OpenClawChatViewModel { func performCompact() async { guard !self.isCompacting else { return } - guard !self.isSending, !self.hasBlockingRunActivity, !self.isAborting else { + guard !self.isSending, !hasBlockingRunActivity, !self.isAborting else { self.errorText = "Wait for the current response before compacting the session." return } diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/CleanChatComposerControls.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/CleanChatComposerControls.swift new file mode 100644 index 000000000000..f12118db2510 --- /dev/null +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/CleanChatComposerControls.swift @@ -0,0 +1,337 @@ +import SwiftUI + +#if !os(macOS) +import PhotosUI +#if canImport(UIKit) +import UIKit +#endif +#endif + +struct CleanChatComposerSurface: ViewModifier { + let cornerRadius: CGFloat + + func body(content: Content) -> some View { + #if os(macOS) + content + .background( + RoundedRectangle(cornerRadius: self.cornerRadius, style: .continuous) + .fill(OpenClawChatTheme.composerField)) + .overlay( + RoundedRectangle(cornerRadius: self.cornerRadius, style: .continuous) + .strokeBorder(OpenClawChatTheme.composerBorder, lineWidth: 1)) + #else + if #available(iOS 26.0, *) { + content + .glassEffect(.regular, in: .rect(cornerRadius: self.cornerRadius)) + } else { + content + .background( + .regularMaterial, + in: RoundedRectangle(cornerRadius: self.cornerRadius, style: .continuous)) + .overlay( + RoundedRectangle(cornerRadius: self.cornerRadius, style: .continuous) + .strokeBorder(OpenClawChatTheme.composerBorder, lineWidth: 1)) + } + #endif + } +} + +enum CleanChatComposerMetrics { + static let controlHeight: CGFloat = 44 +} + +struct CompactChatAttachmentLabel: View { + var body: some View { + Image(systemName: "plus") + .font(OpenClawChatTypography.display(size: 15, weight: .semibold, relativeTo: .subheadline)) + .foregroundStyle(.secondary) + .frame(width: 44, height: 44) + .contentShape(Rectangle()) + } +} + +struct OpenClawChatAttachmentsStrip: View { + let attachments: [OpenClawPendingAttachment] + let onRemove: @MainActor (OpenClawPendingAttachment.ID) -> Void + + var body: some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 6) { + ForEach(self.attachments, id: \OpenClawPendingAttachment.id) { attachment in + HStack(spacing: 6) { + if let image = attachment.preview { + OpenClawPlatformImageFactory.image(image) + .resizable() + .scaledToFill() + .frame(width: 22, height: 22) + .clipShape(RoundedRectangle(cornerRadius: 6, style: .continuous)) + } else if attachment.mimeType.hasPrefix("audio/") { + Image(systemName: "waveform") + Text("Voice note") + .font(OpenClawChatTypography.caption) + if let duration = attachment.durationSeconds { + Text(openClawVoiceNoteDurationLabel(duration)) + .font(OpenClawChatTypography.caption) + .foregroundStyle(.secondary) + } + } else { + Image(systemName: "photo") + Text(attachment.fileName) + .font(OpenClawChatTypography.caption) + .lineLimit(1) + } + + if attachment.preview != nil { + Text(attachment.fileName) + .font(OpenClawChatTypography.caption) + .lineLimit(1) + } + + Button { + self.onRemove(attachment.id) + } label: { + Image(systemName: "xmark.circle.fill") + } + .buttonStyle(.plain) + } + .padding(.horizontal, 8) + .padding(.vertical, 5) + .background(OpenClawChatTheme.accent.opacity(0.08)) + .clipShape(Capsule()) + } + } + } + } +} + +#if !os(macOS) +struct OpenClawChatAttachmentMenu: View { + @Binding var showsPhotoPicker: Bool + @Binding var showsFileImporter: Bool + @Binding var showsCameraPicker: Bool + let isAttachmentInputEnabled: Bool + + var body: some View { + Menu { + Button { + self.showsPhotoPicker = true + } label: { + Label { + Text("Photo Library") + .font(OpenClawChatTypography.body) + } icon: { + Image(systemName: "photo.on.rectangle") + } + } + + #if canImport(UIKit) + Button { + self.showsCameraPicker = true + } label: { + Label { + Text("Camera") + .font(OpenClawChatTypography.body) + } icon: { + Image(systemName: "camera") + } + } + .disabled(!UIImagePickerController.isSourceTypeAvailable(.camera)) + #endif + + Button { + self.showsFileImporter = true + } label: { + Label { + Text("Choose Image File") + .font(OpenClawChatTypography.body) + } icon: { + Image(systemName: "folder") + } + } + + } label: { + CompactChatAttachmentLabel() + } + .help("Add attachment") + .accessibilityLabel("Add attachment") + .accessibilityIdentifier("chat-attachment-picker") + .buttonStyle(.plain) + .disabled(!self.isAttachmentInputEnabled) + } +} + +#if canImport(UIKit) +struct OpenClawChatCameraPicker: UIViewControllerRepresentable { + let onImage: @MainActor (UIImage) -> Void + @Environment(\.dismiss) private var dismiss + + func makeCoordinator() -> Coordinator { + Coordinator(parent: self) + } + + func makeUIViewController(context: Context) -> UIImagePickerController { + let controller = UIImagePickerController() + controller.sourceType = .camera + controller.delegate = context.coordinator + return controller + } + + func updateUIViewController(_: UIImagePickerController, context _: Context) {} + + final class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate { + let parent: OpenClawChatCameraPicker + + init(parent: OpenClawChatCameraPicker) { + self.parent = parent + } + + func imagePickerController( + _: UIImagePickerController, + didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) + { + if let image = info[.originalImage] as? UIImage { + self.parent.onImage(image) + } + self.parent.dismiss() + } + + func imagePickerControllerDidCancel(_: UIImagePickerController) { + self.parent.dismiss() + } + } +} +#endif +#endif + +struct OpenClawChatMicButton: View { + let dictationControl: OpenClawChatDictationControl? + let voiceNoteControl: OpenClawChatVoiceNoteControl? + let isDictationPending: Bool + let isRealtimeTalkActive: Bool + let isComposerEnabled: Bool + let isAttachmentInputEnabled: Bool + let onStartDictation: @MainActor () -> Void + + var body: some View { + if let voiceNoteControl { + if self.isDictationActionEnabled, let dictationControl { + Menu { + self.voiceNoteAction(voiceNoteControl) + } label: { + self.label + } primaryAction: { + self.performDictationAction() + } + .menuIndicator(.hidden) + .buttonStyle(.plain) + .modifier(UnifiedChatMicMetadata(control: dictationControl)) + } else { + Menu { + self.voiceNoteAction(voiceNoteControl) + } label: { + self.label + } + .menuIndicator(.hidden) + .buttonStyle(.plain) + .disabled(!self.isVoiceNoteRecordingEnabled(voiceNoteControl)) + .accessibilityLabel("Record Voice Note") + .accessibilityIdentifier("chat-dictation-control") + .help("Record Voice Note") + } + } else if let dictationControl { + Button(action: self.performDictationAction) { + self.label + } + .buttonStyle(.plain) + .disabled(!self.isDictationActionEnabled) + .modifier(UnifiedChatMicMetadata(control: dictationControl)) + } + } + + private var label: some View { + Image(systemName: self.dictationControl?.isActive == true ? "stop.fill" : "mic") + .font(OpenClawChatTypography.display(size: 17, weight: .medium, relativeTo: .body)) + .foregroundStyle(self.dictationControl?.isActive == true ? OpenClawChatTheme.accent : .secondary) + .frame(width: 44, height: 44) + .contentShape(Rectangle()) + } + + private func performDictationAction() { + guard let dictationControl else { return } + if dictationControl.isActive { + dictationControl.finish() + } else { + self.onStartDictation() + } + } + + private var isDictationActionEnabled: Bool { + Self.dictationActionEnabled( + isComposerEnabled: self.isComposerEnabled, + isAvailable: self.dictationControl?.isAvailable == true, + isActive: self.dictationControl?.isActive == true, + isTalkActive: self.isRealtimeTalkActive || self.voiceNoteControl?.isTalkActive == true, + isVoiceNoteCaptureActive: self.voiceNoteControl?.recorder.isRecording == true || + self.voiceNoteControl?.recorder.isRequestingPermission == true) + } + + private func voiceNoteAction(_ voiceNoteControl: OpenClawChatVoiceNoteControl) -> some View { + Button { + Task { await voiceNoteControl.recorder.start() } + } label: { + Label("Record Voice Note", systemImage: "waveform") + } + .disabled(!self.isVoiceNoteRecordingEnabled(voiceNoteControl)) + } + + private func isVoiceNoteRecordingEnabled(_ voiceNoteControl: OpenClawChatVoiceNoteControl) -> Bool { + Self.voiceNoteRecordingEnabled( + isComposerEnabled: self.isComposerEnabled, + isAttachmentInputEnabled: self.isAttachmentInputEnabled, + isDictationActive: self.dictationControl?.isActive == true, + isDictationPending: self.isDictationPending, + isTalkActive: self.isRealtimeTalkActive || voiceNoteControl.isTalkActive, + isRecording: voiceNoteControl.recorder.isRecording, + isRequestingPermission: voiceNoteControl.recorder.isRequestingPermission) + } + + nonisolated static func dictationActionEnabled( + isComposerEnabled: Bool, + isAvailable: Bool, + isActive: Bool, + isTalkActive: Bool, + isVoiceNoteCaptureActive: Bool) -> Bool + { + isActive || (isComposerEnabled && isAvailable && !isTalkActive && !isVoiceNoteCaptureActive) + } + + nonisolated static func voiceNoteRecordingEnabled( + isComposerEnabled: Bool, + isAttachmentInputEnabled: Bool, + isDictationActive: Bool, + isDictationPending: Bool, + isTalkActive: Bool, + isRecording: Bool, + isRequestingPermission: Bool) -> Bool + { + isComposerEnabled + && isAttachmentInputEnabled + && !isDictationActive + && !isDictationPending + && !isTalkActive + && !isRecording + && !isRequestingPermission + } +} + +private struct UnifiedChatMicMetadata: ViewModifier { + let control: OpenClawChatDictationControl + + func body(content: Content) -> some View { + content + .accessibilityLabel(self.control.isActive ? "Finish dictation" : "Dictate message") + .accessibilityValue(self.control.isActive ? "Listening" : "Not listening") + .accessibilityIdentifier("chat-dictation-control") + .help(self.control.isActive ? "Finish dictation" : "Transcribe speech into the message") + } +} diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/OpenClawChatDictationControl.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/OpenClawChatDictationControl.swift new file mode 100644 index 000000000000..aea8eea00f90 --- /dev/null +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/OpenClawChatDictationControl.swift @@ -0,0 +1,38 @@ +public struct OpenClawChatDictationControl { + public var isActive: Bool + public var isAvailable: Bool + public var start: @MainActor () async throws -> String? + public var finish: @MainActor () -> Void + public var cancel: @MainActor () -> Void + + public init( + isActive: Bool, + isAvailable: Bool, + start: @escaping @MainActor () async throws -> String?, + finish: @escaping @MainActor () -> Void, + cancel: @escaping @MainActor () -> Void) + { + self.isActive = isActive + self.isAvailable = isAvailable + self.start = start + self.finish = finish + self.cancel = cancel + } +} + +extension OpenClawChatViewModel { + func appendDictationTranscript(_ transcript: String, for session: SessionSnapshot) { + guard self.isCurrentSession(session) else { return } + if self.input.isEmpty { + self.input = transcript + } else { + let separator = self.input.last?.isWhitespace == true ? "" : " " + self.input += separator + transcript + } + } + + func setDictationError(_ error: Error, for session: SessionSnapshot) { + guard self.isCurrentSession(session) else { return } + self.errorText = error.localizedDescription + } +} diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/TalkWaveformView.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/TalkWaveformView.swift index 9f787536685a..563cfc252b48 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/TalkWaveformView.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/TalkWaveformView.swift @@ -1,5 +1,9 @@ import SwiftUI +private enum TalkWaveformClock { + static let born = Date() +} + /// Universal OpenClaw talk animation: an iOS 9-style Siri waveform shared by the /// iOS, watchOS, and macOS apps; the Android app ports the same math in Compose /// (`TalkWaveform.kt`). Math adapted from noahchalifour/swiftui-siri-waveform-view @@ -39,7 +43,7 @@ public struct TalkWaveformPalette: Equatable, Sendable { public static let standard = TalkWaveformPalette( active: [ Color(red: 198 / 255.0, green: 62 / 255.0, blue: 56 / 255.0), - Color(red: 0.95, green: 0.45, blue: 0.30), + Color(red: 79 / 255.0, green: 200 / 255.0, blue: 174 / 255.0), Color(red: 0.45, green: 0.08, blue: 0.12), ], inactive: [ @@ -56,8 +60,6 @@ public struct TalkWaveformView: View { @Environment(\.colorScheme) private var colorScheme @Environment(\.accessibilityReduceMotion) private var reduceMotion - private static let born = Date() - public init(phase: TalkWaveformPhase, palette: TalkWaveformPalette = .standard) { self.phase = phase self.palette = palette @@ -66,7 +68,7 @@ public struct TalkWaveformView: View { public var body: some View { let frozen = self.reduceMotion || self.phase == .idle TimelineView(.animation(minimumInterval: 1.0 / 30.0, paused: frozen)) { timeline in - let time = frozen ? 0 : timeline.date.timeIntervalSince(Self.born) + let time = frozen ? 0 : timeline.date.timeIntervalSince(TalkWaveformClock.born) let power = TalkWaveformMath.power(for: self.phase, time: time) Canvas { context, size in let midY = size.height / 2 @@ -96,6 +98,258 @@ public struct TalkWaveformView: View { } } +/// A continuous, audio-reactive contour that turns an avatar into the voice +/// surface. Unlike radial equalizer bars, the two overlapping lobes read as one +/// living signal and stay legible at Dynamic Island scale. +public struct TalkAvatarWaveformView: View { + public var phase: TalkWaveformPhase + public var palette: TalkWaveformPalette + public var diameter: CGFloat + public var avatarDiameter: CGFloat + public var samples: [Double] + + @Environment(\.accessibilityReduceMotion) private var reduceMotion + @State private var capturedSamples: [Double] = [] + private let avatar: Avatar + + public init( + phase: TalkWaveformPhase, + palette: TalkWaveformPalette = .standard, + diameter: CGFloat, + avatarDiameter: CGFloat, + samples: [Double] = [], + @ViewBuilder avatar: () -> Avatar) + { + self.phase = phase + self.palette = palette + self.diameter = diameter + self.avatarDiameter = avatarDiameter + self.samples = samples + self.avatar = avatar() + } + + public var body: some View { + let isActive = self.phase != .idle + + ZStack { + if self.usesMeasuredEnvelope { + self.measuredContour + } else { + self.fallbackContour(isActive: isActive) + } + + self.avatar + .frame(width: self.avatarDiameter, height: self.avatarDiameter) + } + .frame(width: self.diameter, height: self.diameter) + .animation(self.reduceMotion ? nil : .easeOut(duration: 0.2), value: isActive) + .onAppear { self.capture(self.measuredLevel) } + .onChange(of: self.measuredLevel) { _, level in + self.capture(level) + } + } + + private var measuredContour: some View { + let colors = self.contourColors + let renderedSamples = self.reduceMotion ? [0.16] : self.renderedSamples + let previousSamples = renderedSamples.count > 1 + ? Array(renderedSamples.dropLast()) + : renderedSamples + + return Canvas { context, size in + let center = CGPoint(x: size.width / 2, y: size.height / 2) + let baseRadius = self.avatarDiameter / 2 + max(2, self.diameter * 0.035) + let availableAmplitude = max(1, min(size.width, size.height) / 2 - baseRadius - 1) + + let backPath = TalkWaveformMath.radialEnvelopePath( + center: center, + baseRadius: baseRadius + 0.5, + amplitude: availableAmplitude, + samples: previousSamples, + scale: 0.72) + context.fill(backPath, with: .color(colors.secondary.opacity(0.36))) + + let frontPath = TalkWaveformMath.radialEnvelopePath( + center: center, + baseRadius: baseRadius, + amplitude: availableAmplitude, + samples: renderedSamples) + context.fill( + frontPath, + with: .linearGradient( + Gradient(colors: [colors.primary, colors.primary, colors.secondary]), + startPoint: CGPoint(x: 0, y: size.height), + endPoint: CGPoint(x: size.width, y: 0))) + } + .shadow(color: colors.primary.opacity(0.22), radius: max(2, self.diameter * 0.06)) + .animation(self.reduceMotion ? nil : .linear(duration: 0.12), value: renderedSamples) + } + + private func fallbackContour(isActive: Bool) -> some View { + let frozen = self.reduceMotion || !isActive + let colors = self.contourColors + + return TimelineView(.animation(minimumInterval: 1.0 / 30.0, paused: frozen)) { timeline in + let time = frozen ? 0 : timeline.date.timeIntervalSince(TalkWaveformClock.born) + let power = TalkWaveformMath.power(for: self.phase, time: time) + + Canvas { context, size in + let center = CGPoint(x: size.width / 2, y: size.height / 2) + let baseRadius = self.avatarDiameter / 2 + max(2, self.diameter * 0.035) + let availableAmplitude = max(1, min(size.width, size.height) / 2 - baseRadius - 1) + + let backPath = TalkWaveformMath.radialPath( + center: center, + baseRadius: baseRadius + 0.75, + amplitude: availableAmplitude * power * 0.82, + time: time, + seed: 3.7) + context.fill(backPath, with: .color(colors.secondary.opacity(0.42))) + + let frontPath = TalkWaveformMath.radialPath( + center: center, + baseRadius: baseRadius, + amplitude: availableAmplitude * power, + time: time, + seed: 0.4) + context.fill( + frontPath, + with: .linearGradient( + Gradient(colors: [colors.primary, colors.primary, colors.secondary]), + startPoint: CGPoint(x: 0, y: size.height), + endPoint: CGPoint(x: size.width, y: 0))) + } + .shadow(color: colors.primary.opacity(0.22), radius: max(2, self.diameter * 0.06)) + } + .opacity(isActive ? 1 : 0) + } + + private var measuredLevel: Double? { + switch self.phase { + case let .listening(level, _): level + case let .speaking(level): level + case .idle, .thinking: nil + } + } + + private var usesMeasuredEnvelope: Bool { + !self.samples.isEmpty || self.measuredLevel != nil + } + + private var renderedSamples: [Double] { + if !self.samples.isEmpty { return self.samples } + if !self.capturedSamples.isEmpty { return self.capturedSamples } + return self.measuredLevel.map { [$0] } ?? [0] + } + + private func capture(_ level: Double?) { + guard self.samples.isEmpty, let level, level.isFinite else { return } + self.capturedSamples.append(min(max(level, 0), 1)) + if self.capturedSamples.count > 16 { + self.capturedSamples.removeFirst(self.capturedSamples.count - 16) + } + } + + private var contourColors: (primary: Color, secondary: Color) { + let primary = self.palette.active.first ?? .red + let secondary = self.palette.active.dropFirst().first ?? primary + switch self.phase { + case .listening: + return (secondary, primary) + case .idle, .thinking, .speaking: + return (primary, secondary) + } + } +} + +/// A compact, center-origin voice envelope for constrained surfaces such as +/// Dynamic Island. Its geometry comes only from recent audio-level samples; +/// there is no time-based carrier, GIF-like loop, or layout-changing width. +/// The widget and watch targets compile this source directly; keep the trace +/// module-internal instead of expanding OpenClawChatUI's public API. +struct TalkVoiceTraceView: View { + var phase: TalkWaveformPhase + var palette: TalkWaveformPalette + var samples: [Double] + var sampleRange: ClosedRange + + @Environment(\.accessibilityReduceMotion) private var reduceMotion + + init( + phase: TalkWaveformPhase, + palette: TalkWaveformPalette = .standard, + samples: [Double] = [], + sampleRange: ClosedRange = 0...1) + { + self.phase = phase + self.palette = palette + self.samples = samples + self.sampleRange = sampleRange + } + + var body: some View { + let isActive = self.phase != .idle + let colors = self.traceColors + let renderedSamples = self.reduceMotion ? [0.16] : (self.samples.isEmpty ? [0.03] : self.samples) + let previousSamples = renderedSamples.count > 1 + ? Array(renderedSamples.dropLast()) + : renderedSamples + + Canvas { context, size in + let middle = size.height / 2 + var baseline = Path() + baseline.move(to: CGPoint(x: 0, y: middle)) + baseline.addLine(to: CGPoint(x: size.width, y: middle)) + context.stroke( + baseline, + with: .linearGradient( + Gradient(colors: [ + colors.primary.opacity(0), + colors.secondary.opacity(0.36), + colors.primary.opacity(0), + ]), + startPoint: .zero, + endPoint: CGPoint(x: size.width, y: 0)), + lineWidth: 0.75) + + // The back contour is the immediately preceding real envelope, + // creating depth without introducing an unrelated oscillator. + let backPath = TalkWaveformMath.traceEnvelopePath( + in: size, + samples: previousSamples, + sampleRange: self.sampleRange, + scale: 0.72) + context.fill(backPath, with: .color(colors.secondary.opacity(0.28))) + + let frontPath = TalkWaveformMath.traceEnvelopePath( + in: size, + samples: renderedSamples, + sampleRange: self.sampleRange, + scale: 1) + context.fill( + frontPath, + with: .linearGradient( + Gradient(colors: [colors.primary.opacity(0.82), colors.secondary, colors.primary.opacity(0.82)]), + startPoint: CGPoint(x: 0, y: middle), + endPoint: CGPoint(x: size.width, y: middle))) + } + .shadow(color: colors.primary.opacity(0.22), radius: 2) + .opacity(isActive ? 1 : 0) + .accessibilityHidden(true) + } + + private var traceColors: (primary: Color, secondary: Color) { + let primary = self.palette.active.first ?? .red + let secondary = self.palette.active.dropFirst().first ?? primary + switch self.phase { + case .listening: + return (secondary, primary) + case .idle, .thinking, .speaking: + return (primary, secondary) + } + } +} + /// Pure waveform math, split from the view for unit testing and so the Android /// port has one canonical reference for every constant. enum TalkWaveformMath { @@ -162,6 +416,170 @@ enum TalkWaveformMath { return path } + /// Closed organic contour used around the avatar. The harmonic mix avoids + /// spoke-like repetition while remaining deterministic and bounded. + static func radialPath( + center: CGPoint, + baseRadius: CGFloat, + amplitude: CGFloat, + time: Double, + seed: Double, + sampleCount: Int = 96) -> Path + { + let count = max(sampleCount, 24) + var points: [CGPoint] = [] + points.reserveCapacity(count) + + for index in 0.. Double + { + let harmonic = 0.50 + + 0.22 * sin(angle * 3 + time * 2.35 + seed) + + 0.17 * sin(angle * 5 - time * 1.55 + seed * 1.7) + + 0.11 * sin(angle * 7 + time * 0.95 - seed * 0.6) + let normalized = min(max(harmonic, 0), 1) + return baseRadius + max(amplitude, 0) * (0.30 + 0.70 * normalized) + } + + /// A closed contour made only from measured audio history. The newest sample + /// sits opposite the seam and the history mirrors back toward it, keeping the + /// path continuous without a time-based carrier. + static func radialEnvelopePath( + center: CGPoint, + baseRadius: CGFloat, + amplitude: CGFloat, + samples: [Double], + scale: Double = 1, + sampleCount: Int = 72) -> Path + { + let count = max(sampleCount, 24) + var path = Path() + + for index in 0.. Double { + guard !samples.isEmpty else { return 0.08 } + let x = min(max(progress, 0), 1) + let mirroredHistory = abs(x * 2 - 1) * Double(max(samples.count - 1, 0)) + let level = Self.interpolatedEnvelopeSample(at: mirroredHistory, samples: samples) + return 0.08 + 0.92 * pow(level, 0.72) + } + + /// A closed envelope whose newest audio sample lives at the center and whose + /// prior samples radiate toward both edges. `sampleRange` lets constrained + /// surfaces render a focused segment without changing the underlying signal. + static func traceEnvelopePath( + in size: CGSize, + samples: [Double], + sampleRange: ClosedRange = 0...1, + scale: Double = 1, + sampleCount: Int = 48) -> Path + { + let count = max(sampleCount, 16) + let midY = Double(size.height) / 2 + let halfHeight = max(1, midY - 1) + var upper: [CGPoint] = [] + upper.reserveCapacity(count + 1) + + for index in 0...count { + let localProgress = Double(index) / Double(count) + let progress = sampleRange.lowerBound + localProgress * (sampleRange.upperBound - sampleRange.lowerBound) + let magnitude = Self.traceEnvelopeMagnitude(progress: progress, samples: samples) + let displacement = halfHeight * min(max(scale, 0), 1) * magnitude + upper.append(CGPoint( + x: Double(size.width) * localProgress, + y: midY - displacement)) + } + + var path = Path() + guard let first = upper.first else { return path } + path.move(to: first) + path.addLines(Array(upper.dropFirst())) + for point in upper.reversed() { + path.addLine(to: CGPoint(x: point.x, y: 2 * midY - point.y)) + } + path.closeSubpath() + return path + } + + static func traceEnvelopeMagnitude(progress: Double, samples: [Double]) -> Double { + let x = min(max(progress, 0), 1) + guard x > 0, x < 1 else { return 0 } + let taper = pow(max(0, sin(.pi * x)), 0.48) + guard !samples.isEmpty else { return 0.03 * taper } + + let distanceFromCenter = abs(x - 0.5) * 2 + let historyPosition = distanceFromCenter * Double(max(samples.count - 1, 0)) + let level = Self.interpolatedEnvelopeSample(at: historyPosition, samples: samples) + return taper * (0.03 + 0.97 * pow(level, 0.72)) + } + + /// Catmull-Rom interpolation keeps sparse ActivityKit samples fluid while + /// remaining entirely derived from the measured playback history. + static func interpolatedEnvelopeSample(at historyPosition: Double, samples: [Double]) -> Double { + guard !samples.isEmpty else { return 0 } + let maximum = samples.count - 1 + let position = min(max(historyPosition, 0), Double(maximum)) + let lower = Int(position.rounded(.down)) + let fraction = position - Double(lower) + + func newestFirst(_ index: Int) -> Double { + let clampedIndex = min(max(index, 0), maximum) + return min(max(samples[maximum - clampedIndex], 0), 1) + } + + let p0 = newestFirst(lower - 1) + let p1 = newestFirst(lower) + let p2 = newestFirst(lower + 1) + let p3 = newestFirst(lower + 2) + let value = 0.5 * ((2 * p1) + + (-p0 + p2) * fraction + + (2 * p0 - 5 * p1 + 4 * p2 - p3) * fraction * fraction + + (-p0 + 3 * p1 - 3 * p2 + p3) * fraction * fraction * fraction) + return min(max(value, 0), 1) + } + /// |A·sin(kx − t)| shaped by the bell envelope g = (K/(K+(kx−t′)²))^K, K = 4. private static func attenuatedSine(x: Double, A: Double, k: Double, t: Double) -> Double { let sine = A * sin(k * x - t) diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/VoiceNoteComposerViews.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/VoiceNoteComposerViews.swift index 4297cdafda25..031e4564ee54 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/VoiceNoteComposerViews.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/VoiceNoteComposerViews.swift @@ -49,6 +49,7 @@ struct OpenClawVoiceNoteButton: View { struct OpenClawVoiceNoteRecordingRow: View { let recorder: OpenClawVoiceNoteRecorder + var embedded = false var body: some View { HStack(spacing: 10) { @@ -86,14 +87,17 @@ struct OpenClawVoiceNoteRecordingRow: View { .controlSize(.small) .accessibilityLabel("Finish voice note") } - .padding(.horizontal, 14) - .padding(.vertical, 10) - .background( - RoundedRectangle(cornerRadius: 14, style: .continuous) - .fill(OpenClawChatTheme.composerField) - .overlay( - RoundedRectangle(cornerRadius: 14, style: .continuous) - .strokeBorder(OpenClawChatTheme.composerBorder))) + .padding(.horizontal, self.embedded ? 2 : 14) + .padding(.vertical, self.embedded ? 2 : 10) + .background { + if !self.embedded { + RoundedRectangle(cornerRadius: 14, style: .continuous) + .fill(OpenClawChatTheme.composerField) + .overlay( + RoundedRectangle(cornerRadius: 14, style: .continuous) + .strokeBorder(OpenClawChatTheme.composerBorder)) + } + } } } diff --git a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatComposerParityTests.swift b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatComposerParityTests.swift index cee89d1beaff..98855310466a 100644 --- a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatComposerParityTests.swift +++ b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatComposerParityTests.swift @@ -175,6 +175,24 @@ struct ChatComposerStateTests { #expect(vm.input.isEmpty) } + @Test func `attachment staging blocks UI and programmatic sends`() async throws { + let transport = ComposerParityTransport() + let vm = OpenClawChatViewModel(sessionKey: "main", transport: transport) + vm.healthOK = true + vm.input = "send with selected image" + vm.beginAttachmentStaging() + + #expect(!vm.canSend) + vm.send() + await Task.yield() + #expect(transport.sentMessages.isEmpty) + + vm.endAttachmentStaging() + #expect(vm.canSend) + vm.send() + try await waitUntil("post-staging send accepted") { transport.sentMessages.count == 1 } + } + @Test func `slash send ignores and preserves reply target`() async throws { let transport = ComposerParityTransport() let vm = OpenClawChatViewModel(sessionKey: "main", transport: transport) diff --git a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatViewModelTests.swift b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatViewModelTests.swift index e72a71c4e24e..d04245c6333b 100644 --- a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatViewModelTests.swift +++ b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatViewModelTests.swift @@ -226,6 +226,26 @@ private func commandChoice( acceptsArgs: acceptsArgs) } +private struct ToolActivityEvent: Equatable { + var id: String + var name: String + var isActive: Bool + var sessionKey: String +} + +@MainActor +private final class ToolActivityRecorder { + private(set) var events: [ToolActivityEvent] = [] + + func record(id: String, name: String, isActive: Bool, sessionKey: String) { + self.events.append(ToolActivityEvent( + id: id, + name: name, + isActive: isActive, + sessionKey: sessionKey)) + } +} + @MainActor private func makeViewModel( sessionKey: String = "main", @@ -262,6 +282,11 @@ private func makeViewModel( modelPickerStore: ChatModelPickerStore? = nil, onSessionChanged: (@MainActor (String) -> Void)? = nil, onThinkingLevelChanged: (@MainActor @Sendable (String) -> Void)? = nil, + onToolActivity: (@MainActor @Sendable ( + _ id: String, + _ name: String, + _ isActive: Bool, + _ sessionKey: String) -> Void)? = nil, onThinkingPreferenceChanged: (@MainActor @Sendable (String?) -> Void)? = nil, onVerboseLevelChanged: (@MainActor @Sendable (String) -> Void)? = nil, onVerbosePreferenceChanged: (@MainActor @Sendable (String?) -> Void)? = nil) async @@ -308,6 +333,7 @@ private func makeViewModel( initialVerboseLevel: initialVerboseLevel, onSessionChanged: onSessionChanged, onThinkingLevelChanged: onThinkingLevelChanged, + onToolActivity: onToolActivity, onThinkingPreferenceChanged: onThinkingPreferenceChanged, onVerboseLevelChanged: onVerboseLevelChanged, onVerbosePreferenceChanged: onVerbosePreferenceChanged) @@ -917,7 +943,7 @@ private final class TestChatTransport: @unchecked Sendable, OpenClawChatTranspor code: 0, userInfo: [NSLocalizedDescriptionKey: "thinkingLevel cannot be cleared"]) } - let thinkingResult = try await self.patchSessionThinking( + let thinkingResult = try await patchSessionThinking( sessionKey: sessionKey, thinkingLevel: thinkingLevel) result = OpenClawChatModelPatchResult( @@ -934,7 +960,7 @@ private final class TestChatTransport: @unchecked Sendable, OpenClawChatTranspor sessionKey: String, thinkingLevel: String) async throws -> OpenClawChatModelPatchResult? { - let index = await self.state.recordPatchedThinkingLevel(thinkingLevel) + let index = await state.recordPatchedThinkingLevel(thinkingLevel) if let setSessionThinkingHook { try await setSessionThinkingHook(thinkingLevel) } @@ -2806,6 +2832,220 @@ struct ChatViewModelTests { #expect(await MainActor.run { vm.pendingToolCalls.isEmpty }) } + @Test func `dictation completion only updates its originating session`() async { + let (_, vm) = await makeViewModel( + historyResponses: [historyPayload(sessionKey: "other", sessionId: "sess-other")]) + await MainActor.run { + let startingSession = vm.currentSessionSnapshot() + vm.switchSession(to: "other") + vm.appendDictationTranscript("belongs to main", for: startingSession) + #expect(vm.input.isEmpty) + vm.appendDictationTranscript("belongs to other", for: vm.currentSessionSnapshot()) + #expect(vm.input == "belongs to other") + } + } + + @Test func `dictation completion cannot cross a same-session agent change`() async { + let (_, vm) = await makeViewModel( + activeAgentId: "alpha", + historyResponses: [historyPayload(), historyPayload()]) + await MainActor.run { + let alphaSession = vm.currentSessionSnapshot() + vm.syncActiveAgentId("beta") + vm.appendDictationTranscript("belongs to alpha", for: alphaSession) + #expect(vm.input.isEmpty) + vm.appendDictationTranscript("belongs to beta", for: vm.currentSessionSnapshot()) + #expect(vm.input == "belongs to beta") + } + } + + @Test func `dictation failure only updates its originating session`() async { + let (_, vm) = await makeViewModel( + historyResponses: [historyPayload(sessionKey: "other", sessionId: "sess-other")]) + await MainActor.run { + let startingSession = vm.currentSessionSnapshot() + vm.switchSession(to: "other") + vm.setDictationError( + NSError(domain: "Dictation", code: 1, userInfo: [ + NSLocalizedDescriptionKey: "stale dictation failure", + ]), + for: startingSession) + #expect(vm.errorText == nil) + } + } + + @Test func `composer presentation owner changes when the model is replaced for the same session`() async { + let (_, first) = await makeViewModel(historyResponses: [historyPayload()]) + let (_, replacement) = await makeViewModel(historyResponses: [historyPayload()]) + + let firstOwner = await MainActor.run { + OpenClawChatComposerPresentationOwner(viewModel: first) + } + let replacementOwner = await MainActor.run { + OpenClawChatComposerPresentationOwner(viewModel: replacement) + } + + #expect(firstOwner.session.key == replacementOwner.session.key) + #expect(firstOwner != replacementOwner) + } + + @Test func `composer presentation owner changes with same-session agent routing`() async { + let (_, vm) = await makeViewModel( + activeAgentId: "alpha", + historyResponses: [historyPayload(), historyPayload()]) + + let alphaOwner = await MainActor.run { + OpenClawChatComposerPresentationOwner(viewModel: vm) + } + let betaOwner = await MainActor.run { + vm.syncActiveAgentId("beta") + return OpenClawChatComposerPresentationOwner(viewModel: vm) + } + + #expect(alphaOwner.session.key == betaOwner.session.key) + #expect(alphaOwner != betaOwner) + } + + @Test func `camera attachment completion only updates its originating session`() async { + let (_, vm) = await makeViewModel( + historyResponses: [historyPayload(sessionKey: "other", sessionId: "sess-other")]) + let originalSession = await MainActor.run { + let session = vm.currentSessionSnapshot() + vm.switchSession(to: "other") + return session + } + + await vm.addImageAttachment( + data: Data([0]), + fileName: "stale-camera.jpg", + mimeType: "image/jpeg", + for: originalSession) + + #expect(await MainActor.run { vm.attachments.isEmpty }) + #expect(await MainActor.run { vm.errorText == nil }) + } + + @Test func `camera attachment completion cannot cross a same-session agent change`() async { + let (_, vm) = await makeViewModel( + activeAgentId: "alpha", + historyResponses: [historyPayload(), historyPayload()]) + let alphaSession = await MainActor.run { + let session = vm.currentSessionSnapshot() + vm.syncActiveAgentId("beta") + return session + } + + await vm.addImageAttachment( + data: Data([0]), + fileName: "stale-camera.jpg", + mimeType: "image/jpeg", + for: alphaSession) + + #expect(await MainActor.run { vm.attachments.isEmpty }) + #expect(await MainActor.run { vm.errorText == nil }) + } + + @Test func `file attachment completion cannot cross a same-session agent change`() async { + let (_, vm) = await makeViewModel( + activeAgentId: "alpha", + historyResponses: [historyPayload(), historyPayload()]) + let alphaSession = await MainActor.run { + let session = vm.currentSessionSnapshot() + vm.syncActiveAgentId("beta") + return session + } + + await vm.loadAttachments( + urls: [URL(fileURLWithPath: "/does-not-exist/stale-file.jpg")], + expectedSession: alphaSession) + + #expect(await MainActor.run { vm.attachments.isEmpty }) + #expect(await MainActor.run { vm.errorText == nil }) + } + + @Test func `attachment staging defers a same-session agent change`() async { + let (_, vm) = await makeViewModel( + activeAgentId: "alpha", + historyResponses: [historyPayload(), historyPayload()]) + + await MainActor.run { + let alphaSession = vm.currentSessionSnapshot() + vm.beginAttachmentStaging() + vm.syncActiveAgentId("beta") + #expect(vm.activeAgentId == "alpha") + #expect(vm.isCurrentSession(alphaSession)) + + vm.endAttachmentStaging() + #expect(vm.activeAgentId == "beta") + #expect(!vm.isCurrentSession(alphaSession)) + } + } + + @Test func `balances tool activity when a terminal event clears pending calls`() async throws { + let sessionId = "sess-main" + let history = historyPayload(sessionId: sessionId) + let recorder = await MainActor.run { ToolActivityRecorder() } + let (transport, vm) = await makeViewModel( + historyResponses: [history, history], + sendMessageStatus: "pending", + onToolActivity: { id, name, isActive, sessionKey in + recorder.record(id: id, name: name, isActive: isActive, sessionKey: sessionKey) + }) + try await loadAndWaitBootstrap(vm: vm, sessionId: sessionId) + await sendUserMessage(vm) + let runId = try await waitForLastSentRunId(transport) + + emitToolStart(transport: transport, runId: runId) + try await waitUntil("tool activity starts") { + await MainActor.run { recorder.events.count == 1 } + } + + transport.emit(.chat(OpenClawChatEventPayload( + runId: runId, + sessionKey: "main", + state: "final", + message: nil, + errorMessage: nil))) + + try await waitUntil("tool activity ends") { + await MainActor.run { recorder.events.count == 2 } + } + #expect(await MainActor.run { recorder.events } == [ + ToolActivityEvent(id: "t1", name: "demo", isActive: true, sessionKey: "main"), + ToolActivityEvent(id: "t1", name: "demo", isActive: false, sessionKey: "main"), + ]) + } + + @Test func `session switch ends tool activity under its original session`() async throws { + let recorder = await MainActor.run { ToolActivityRecorder() } + let (transport, vm) = await makeViewModel( + historyResponses: [ + historyPayload(sessionKey: "main", sessionId: "sess-main"), + historyPayload(sessionKey: "other", sessionId: "sess-other"), + ], + sendMessageStatus: "pending", + onToolActivity: { id, name, isActive, sessionKey in + recorder.record(id: id, name: name, isActive: isActive, sessionKey: sessionKey) + }) + try await loadAndWaitBootstrap(vm: vm, sessionId: "sess-main") + await sendUserMessage(vm) + let runId = try await waitForLastSentRunId(transport) + + emitToolStart(transport: transport, runId: runId) + try await waitUntil("tool activity starts") { + await MainActor.run { recorder.events.count == 1 } + } + await MainActor.run { vm.switchSession(to: "other") } + try await waitUntil("tool activity ends during session switch") { + await MainActor.run { recorder.events.count == 2 } + } + + #expect(await MainActor.run { recorder.events } == [ + ToolActivityEvent(id: "t1", name: "demo", isActive: true, sessionKey: "main"), + ToolActivityEvent(id: "t1", name: "demo", isActive: false, sessionKey: "main"), + ]) + } + @Test func `renders final chat event message when history is stale`() async throws { let sessionId = "sess-main" let history = historyPayload(sessionId: sessionId) @@ -9651,7 +9891,9 @@ struct ChatViewModelTests { }, listSessionsHook: { _ in let call = await listCallCount.increment() - if call == 1 { return initialSessions } + if call == 1 { + return initialSessions + } if call == 2 { await staleListGate.wait() return initialSessions @@ -9711,8 +9953,12 @@ struct ChatViewModelTests { }, listSessionsHook: { _ in let call = await listCallCount.increment() - if call == 1 { return initialSessions } - if call == 2 { await staleListGate.wait() } + if call == 1 { + return initialSessions + } + if call == 2 { + await staleListGate.wait() + } return initialSessions }) @@ -9777,8 +10023,12 @@ struct ChatViewModelTests { }, listSessionsHook: { _ in let call = await listCallCount.increment() - if call == 1 { return initialSessions } - if call == 2 { await staleListGate.wait() } + if call == 1 { + return initialSessions + } + if call == 2 { + await staleListGate.wait() + } return initialSessions }) diff --git a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/TalkWaveformTests.swift b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/TalkWaveformTests.swift index 51bf8683ce05..6b184f3890fc 100644 --- a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/TalkWaveformTests.swift +++ b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/TalkWaveformTests.swift @@ -55,6 +55,91 @@ struct TalkWaveformMathTests { // The fallback must move over time, not freeze. #expect(seen.count > 5) } + + @Test + func `avatar contour remains outside the avatar and inside its amplitude`() { + let baseRadius = 20.0 + let amplitude = 8.0 + + for time in stride(from: 0.0, through: 3.0, by: 0.2) { + for angle in stride(from: 0.0, through: 2 * Double.pi, by: 0.1) { + let radius = TalkWaveformMath.radialRadius( + angle: angle, + baseRadius: baseRadius, + amplitude: amplitude, + time: time, + seed: 0.4) + #expect(radius >= baseRadius) + #expect(radius <= baseRadius + amplitude) + } + } + } + + @Test + func `avatar contour evolves without radial spokes`() { + let first = TalkWaveformMath.radialRadius( + angle: 0.7, + baseRadius: 20, + amplitude: 8, + time: 0, + seed: 0.4) + let later = TalkWaveformMath.radialRadius( + angle: 0.7, + baseRadius: 20, + amplitude: 8, + time: 0.4, + seed: 0.4) + + #expect(abs(first - later) > 0.01) + } + + @Test + func `avatar audio contour is bounded by measured samples`() { + let samples = [0.12, 0.48, 0.9] + for progress in stride(from: 0.0, through: 1.0, by: 0.01) { + let magnitude = TalkWaveformMath.radialEnvelopeMagnitude(progress: progress, samples: samples) + #expect(magnitude >= 0.08) + #expect(magnitude <= 1) + } + } + + @Test + func `avatar audio contour puts the newest level opposite the seam`() { + let samples = [0.1, 0.9] + let seam = TalkWaveformMath.radialEnvelopeMagnitude(progress: 0, samples: samples) + let opposite = TalkWaveformMath.radialEnvelopeMagnitude(progress: 0.5, samples: samples) + #expect(opposite > seam) + #expect(abs(seam - TalkWaveformMath.radialEnvelopeMagnitude(progress: 1, samples: samples)) < 1e-9) + } + + @Test + func `compact audio envelope tapers cleanly at both edges`() { + let samples = [0.15, 0.48, 0.82] + #expect(TalkWaveformMath.traceEnvelopeMagnitude(progress: 0, samples: samples) == 0) + #expect(abs(TalkWaveformMath.traceEnvelopeMagnitude(progress: 1, samples: samples)) < 1e-9) + } + + @Test + func `compact audio envelope breathes from newest real sample at center`() { + let quiet = TalkWaveformMath.traceEnvelopeMagnitude(progress: 0.5, samples: [0.2, 0.1]) + let loud = TalkWaveformMath.traceEnvelopeMagnitude(progress: 0.5, samples: [0.2, 0.9]) + + #expect(loud > quiet) + #expect(loud <= 1) + let left = TalkWaveformMath.traceEnvelopeMagnitude(progress: 0.32, samples: [0.2, 0.9]) + let right = TalkWaveformMath.traceEnvelopeMagnitude(progress: 0.68, samples: [0.2, 0.9]) + #expect(abs(left - right) < 1e-9) + } + + @Test + func `compact audio envelope interpolation is bounded by the measured scale`() { + let samples = [-4, 0.1, 0.8, 3] + for position in stride(from: -2.0, through: 8.0, by: 0.05) { + let value = TalkWaveformMath.interpolatedEnvelopeSample(at: position, samples: samples) + #expect(value >= 0) + #expect(value <= 1) + } + } } struct TalkAudioLevelTests { diff --git a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/VoiceNoteRecorderTests.swift b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/VoiceNoteRecorderTests.swift index 12518bbbd1d2..0402db8d1721 100644 --- a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/VoiceNoteRecorderTests.swift +++ b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/VoiceNoteRecorderTests.swift @@ -223,6 +223,101 @@ final class VoiceNoteRecorderTests: XCTestCase { XCTAssertEqual(openClawVoiceNoteDurationLabel(-1), "0:00") } + func testUnifiedMicKeepsVoiceNoteAvailableWithoutDictation() { + XCTAssertFalse(OpenClawChatMicButton.dictationActionEnabled( + isComposerEnabled: true, + isAvailable: false, + isActive: false, + isTalkActive: false, + isVoiceNoteCaptureActive: false)) + XCTAssertTrue(OpenClawChatMicButton.voiceNoteRecordingEnabled( + isComposerEnabled: true, + isAttachmentInputEnabled: true, + isDictationActive: false, + isDictationPending: false, + isTalkActive: false, + isRecording: false, + isRequestingPermission: false)) + } + + func testUnifiedMicPreventsCompetingVoiceCapture() { + XCTAssertFalse(OpenClawChatMicButton.voiceNoteRecordingEnabled( + isComposerEnabled: true, + isAttachmentInputEnabled: true, + isDictationActive: true, + isDictationPending: false, + isTalkActive: false, + isRecording: false, + isRequestingPermission: false)) + XCTAssertFalse(OpenClawChatMicButton.voiceNoteRecordingEnabled( + isComposerEnabled: true, + isAttachmentInputEnabled: true, + isDictationActive: false, + isDictationPending: true, + isTalkActive: false, + isRecording: false, + isRequestingPermission: false)) + XCTAssertFalse(OpenClawChatMicButton.voiceNoteRecordingEnabled( + isComposerEnabled: true, + isAttachmentInputEnabled: true, + isDictationActive: false, + isDictationPending: false, + isTalkActive: true, + isRecording: false, + isRequestingPermission: false)) + XCTAssertFalse(OpenClawChatMicButton.voiceNoteRecordingEnabled( + isComposerEnabled: true, + isAttachmentInputEnabled: true, + isDictationActive: false, + isDictationPending: false, + isTalkActive: false, + isRecording: true, + isRequestingPermission: false)) + XCTAssertFalse(OpenClawChatMicButton.dictationActionEnabled( + isComposerEnabled: true, + isAvailable: true, + isActive: false, + isTalkActive: true, + isVoiceNoteCaptureActive: false)) + XCTAssertFalse(OpenClawChatMicButton.dictationActionEnabled( + isComposerEnabled: true, + isAvailable: true, + isActive: false, + isTalkActive: false, + isVoiceNoteCaptureActive: true)) + XCTAssertTrue(OpenClawChatMicButton.dictationActionEnabled( + isComposerEnabled: false, + isAvailable: false, + isActive: true, + isTalkActive: true, + isVoiceNoteCaptureActive: true)) + XCTAssertFalse(OpenClawChatMicButton.dictationActionEnabled( + isComposerEnabled: false, + isAvailable: true, + isActive: false, + isTalkActive: false, + isVoiceNoteCaptureActive: false)) + } + + func testCompactTalkControlYieldsToLocalVoiceCapture() { + XCTAssertTrue(OpenClawChatComposer.showsCompactTalkControl( + hasDraftToSend: false, + hasBlockingRunActivity: false, + isLocalVoiceCaptureActive: false)) + XCTAssertFalse(OpenClawChatComposer.showsCompactTalkControl( + hasDraftToSend: false, + hasBlockingRunActivity: false, + isLocalVoiceCaptureActive: true)) + XCTAssertFalse(OpenClawChatComposer.showsCompactTalkControl( + hasDraftToSend: true, + hasBlockingRunActivity: false, + isLocalVoiceCaptureActive: false)) + XCTAssertFalse(OpenClawChatComposer.showsCompactTalkControl( + hasDraftToSend: false, + hasBlockingRunActivity: true, + isLocalVoiceCaptureActive: false)) + } + @MainActor func testRecordingPublishesCaptureLevelsAndResetsOnFinish() async throws { let capture = FakeVoiceNoteAudioCapture() diff --git a/test/scripts/native-app-i18n.test.ts b/test/scripts/native-app-i18n.test.ts index f2a6db3e6df8..3a4d7516264e 100644 --- a/test/scripts/native-app-i18n.test.ts +++ b/test/scripts/native-app-i18n.test.ts @@ -467,13 +467,6 @@ describe("native app i18n inventory", () => { "Cron changes require operator.admin. Setup codes intentionally do not grant it. Reconnect with the gateway's shared token or password to request admin access. If this device still lacks it, approve the pending scope upgrade from an existing admin client.", ), ).toBe(true); - expect( - entries.some( - (entry) => - entry.source === - "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from this device to the voice provider.", - ), - ).toBe(true); expect( entries.some( (entry) => @@ -493,7 +486,6 @@ describe("native app i18n inventory", () => { [ "Your AI-powered setup helper. It can check status, fix config, ", "Cron changes require operator.admin. Setup codes intentionally do not grant it. ", - "This device needs gateway approval before Talk can use realtime voice. Audio will go directly from ", "Writes a rotating, local-only log under ~/Library/Logs/OpenClaw/. ", "Paste the token configured on the gateway host. ", ].includes(entry.source),