mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
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
This commit is contained in:
@@ -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",
|
||||
"บันทึกเสียง"
|
||||
"ข้อความเสียง"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
+596
-692
File diff suppressed because it is too large
Load Diff
+190
-250
@@ -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",
|
||||
|
||||
+190
-250
@@ -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",
|
||||
|
||||
+190
-250
@@ -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",
|
||||
|
||||
+190
-250
@@ -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",
|
||||
|
||||
+190
-250
@@ -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",
|
||||
|
||||
+190
-250
@@ -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",
|
||||
|
||||
+190
-250
@@ -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",
|
||||
|
||||
+190
-250
@@ -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",
|
||||
|
||||
+190
-250
@@ -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",
|
||||
|
||||
+190
-250
@@ -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",
|
||||
|
||||
+190
-250
@@ -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",
|
||||
|
||||
+190
-250
@@ -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",
|
||||
|
||||
+190
-250
@@ -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",
|
||||
|
||||
+190
-250
@@ -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",
|
||||
|
||||
+190
-250
@@ -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",
|
||||
|
||||
+190
-250
@@ -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",
|
||||
|
||||
+190
-250
@@ -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",
|
||||
|
||||
+190
-250
@@ -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",
|
||||
|
||||
+190
-250
@@ -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",
|
||||
|
||||
+190
-250
@@ -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",
|
||||
|
||||
+190
-250
@@ -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",
|
||||
|
||||
@@ -105,7 +105,7 @@
|
||||
<string name="native_100ac08064a6d586" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"لا توجد جلسات حديثة"</string>
|
||||
<string name="native_1039e92fa05c1279" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"حالة Gateway، وجاهزية عقدة الهاتف، وتدفق السجلات الأخير."</string>
|
||||
<string name="native_106808530413298b" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"فتح تفاصيل الأتمتة"</string>
|
||||
<string name="native_1093115897879aa3" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"وقت التشغيل"</string>
|
||||
<string name="native_1093115897879aa3" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"بيئة التشغيل"</string>
|
||||
<string name="native_1102bd820f68901b" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"الوكيل %1$s"</string>
|
||||
<string name="native_11252dd2410324bc" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Get Goal"</string>
|
||||
<string name="native_114c9789813fd2b0" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"OpenClaw %1$s (%2$s)"</string>
|
||||
|
||||
@@ -854,7 +854,7 @@
|
||||
<string name="native_91e9086800ed776e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Nessuna attività pianificata"</string>
|
||||
<string name="native_920e413c7d411b61" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Stato"</string>
|
||||
<string name="native_921ff8493d077a59" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Nodo OpenClaw · Connesso"</string>
|
||||
<string name="native_92340695899bd2d8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Attivo"</string>
|
||||
<string name="native_92340695899bd2d8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Attive"</string>
|
||||
<string name="native_92458e650bc168c5" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Mostra lo stato di debug della condivisione dello schermo."</string>
|
||||
<string name="native_926373f4aefa7fb9" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Nessun limite segnalato"</string>
|
||||
<string name="native_929d4aafa630ff10" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Chiudi scanner"</string>
|
||||
|
||||
@@ -854,7 +854,7 @@
|
||||
<string name="native_91e9086800ed776e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"スケジュール済みのジョブはありません"</string>
|
||||
<string name="native_920e413c7d411b61" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"ステータス"</string>
|
||||
<string name="native_921ff8493d077a59" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"OpenClaw Node · 接続済み"</string>
|
||||
<string name="native_92340695899bd2d8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"アクティブ"</string>
|
||||
<string name="native_92340695899bd2d8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"有効"</string>
|
||||
<string name="native_92458e650bc168c5" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"画面共有のデバッグ状態を表示します。"</string>
|
||||
<string name="native_926373f4aefa7fb9" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"制限は報告されていません"</string>
|
||||
<string name="native_929d4aafa630ff10" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"スキャナーを閉じる"</string>
|
||||
|
||||
@@ -398,7 +398,7 @@
|
||||
<string name="native_4441146b0fe1d5c6" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"screenshot"</string>
|
||||
<string name="native_4481c2190a1b4bca" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Wyrażenie cron, np. 0 9 * * *"</string>
|
||||
<string name="native_4499e7b7e1c9ddbc" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Powrót do głosu"</string>
|
||||
<string name="native_449f5a775762cdc5" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Rozmowa"</string>
|
||||
<string name="native_449f5a775762cdc5" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Rozmawiaj"</string>
|
||||
<string name="native_45a64dc17588f2ca" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"%1$s/%2$s online"</string>
|
||||
<string name="native_45a8847c4a6b2c81" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Zezwolono %1$s aplikacjom na przekazywanie."</string>
|
||||
<string name="native_460b3a7da007b7af" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Czat"</string>
|
||||
|
||||
@@ -866,7 +866,7 @@
|
||||
<string name="native_93bf9fb90d28b037" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"%1$s / %2$s"</string>
|
||||
<string name="native_93c59f1918d177c6" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Çalışmayı planlayın"</string>
|
||||
<string name="native_93d8874c8c86f0fc" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"console"</string>
|
||||
<string name="native_942087cc2d41e013" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Yeniden dene"</string>
|
||||
<string name="native_942087cc2d41e013" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Yeniden Dene"</string>
|
||||
<string name="native_943a630198a15ae2" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Bir sohbet başlatın; etkin OpenClaw konuşmalarınız burada görünecek."</string>
|
||||
<string name="native_9446c09dff7cb76a" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Otomasyon yüklenemedi."</string>
|
||||
<string name="native_944946b3f36d1520" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Aracı çalışma alanındaki kabuk"</string>
|
||||
|
||||
@@ -854,7 +854,7 @@
|
||||
<string name="native_91e9086800ed776e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"沒有排程工作"</string>
|
||||
<string name="native_920e413c7d411b61" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"狀態"</string>
|
||||
<string name="native_921ff8493d077a59" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"OpenClaw 節點 · 已連線"</string>
|
||||
<string name="native_92340695899bd2d8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"使用中"</string>
|
||||
<string name="native_92340695899bd2d8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"啟用中"</string>
|
||||
<string name="native_92458e650bc168c5" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"顯示螢幕分享偵錯狀態。"</string>
|
||||
<string name="native_926373f4aefa7fb9" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"未回報限制"</string>
|
||||
<string name="native_929d4aafa630ff10" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"關閉掃描器"</string>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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<OpenClawActivityAttributes>) -> some View {
|
||||
private func lockScreenView(
|
||||
context: ActivityViewContext<OpenClawActivityAttributes>,
|
||||
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<OpenClawActivityAttributes>,
|
||||
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<Double> = 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>) -> 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,
|
||||
])
|
||||
}
|
||||
|
||||
+2247
-3335
File diff suppressed because it is too large
Load Diff
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Content: View>: 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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<Bool> {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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<OpenClawActivityAttributes>?
|
||||
private var activityStartDate: Date = .now
|
||||
private var currentState: OpenClawActivityAttributes.ContentState?
|
||||
private var currentStaleDate: Date?
|
||||
private var pendingActivityUpdate: PendingActivityUpdate?
|
||||
private var activityUpdateTask: Task<Void, Never>?
|
||||
private var activityGeneration: UInt64 = 0
|
||||
private var voiceSampleBuffer = LiveActivityVoiceSampleBuffer()
|
||||
private var pendingVoiceSample: UInt8?
|
||||
private var voiceSampleTask: Task<Void, Never>?
|
||||
private var voiceStaleRefreshTask: Task<Void, Never>?
|
||||
private var voiceStaleRefreshGeneration: UInt64 = 0
|
||||
private var toolStaleRefreshTask: Task<Void, Never>?
|
||||
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<OpenClawActivityAttributes>,
|
||||
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<OpenClawActivityAttributes>, 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<OpenClawActivityAttributes>.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<OpenClawActivityAttributes>) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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<Void, Never>?
|
||||
@ObservationIgnored private var talkPermissionUpgradeReconnectTask: Task<Void, Never>?
|
||||
private var talkPermissionUpgradeReconnectGeneration: UInt64 = 0
|
||||
private var lastTalkPermissionReconnectAttemptAt: Date?
|
||||
private var voiceWakeSyncTask: Task<Void, Never>?
|
||||
@ObservationIgnored private var cameraHUDDismissTask: Task<Void, Never>?
|
||||
@@ -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<T>(
|
||||
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
|
||||
|
||||
@@ -842,6 +842,8 @@ extension OnboardingWizardView {
|
||||
}
|
||||
}
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.contentShape(Rectangle())
|
||||
.disabled(!self.canConnectManual || self.connectingGateway != nil)
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
+68
-121
@@ -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<Content: View>: 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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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<SidebarDestination> = [.agents]
|
||||
return self.sidebarGroups
|
||||
.map { group in
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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`() {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
],
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Void, Never>?
|
||||
#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<Void, Never>?
|
||||
@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<Bool> {
|
||||
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<Bool> {
|
||||
Binding(
|
||||
get: { self.showsFileImporter },
|
||||
set: { isPresented in
|
||||
if isPresented {
|
||||
self.fileImporterOwner = self.attachmentCaptureOwner()
|
||||
}
|
||||
self.showsFileImporter = isPresented
|
||||
})
|
||||
}
|
||||
|
||||
private var cameraPickerPresentation: Binding<Bool> {
|
||||
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 = []
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<String> = ["/reset", "/clear"]
|
||||
private static let compactTriggers: Set<String> = ["/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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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?
|
||||
|
||||
@@ -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<Void, Never>?
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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<Avatar: View>: 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<Double>
|
||||
|
||||
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
||||
|
||||
init(
|
||||
phase: TalkWaveformPhase,
|
||||
palette: TalkWaveformPalette = .standard,
|
||||
samples: [Double] = [],
|
||||
sampleRange: ClosedRange<Double> = 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..<count {
|
||||
let angle = Double(index) / Double(count) * 2 * Double.pi
|
||||
let radius = Self.radialRadius(
|
||||
angle: angle,
|
||||
baseRadius: Double(baseRadius),
|
||||
amplitude: Double(amplitude),
|
||||
time: time,
|
||||
seed: seed)
|
||||
points.append(CGPoint(
|
||||
x: center.x + CGFloat(cos(angle) * radius),
|
||||
y: center.y + CGFloat(sin(angle) * radius)))
|
||||
}
|
||||
|
||||
var path = Path()
|
||||
guard let first = points.first else { return path }
|
||||
path.move(to: first)
|
||||
path.addLines(Array(points.dropFirst()))
|
||||
path.closeSubpath()
|
||||
return path
|
||||
}
|
||||
|
||||
static func radialRadius(
|
||||
angle: Double,
|
||||
baseRadius: Double,
|
||||
amplitude: Double,
|
||||
time: Double,
|
||||
seed: Double) -> 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..<count {
|
||||
let progress = Double(index) / Double(count)
|
||||
let angle = progress * 2 * Double.pi - Double.pi / 2
|
||||
let magnitude = Self.radialEnvelopeMagnitude(progress: progress, samples: samples)
|
||||
let radius = baseRadius + amplitude * CGFloat(magnitude * min(max(scale, 0), 1))
|
||||
let point = CGPoint(
|
||||
x: center.x + CGFloat(cos(angle)) * radius,
|
||||
y: center.y + CGFloat(sin(angle)) * radius)
|
||||
if index == 0 {
|
||||
path.move(to: point)
|
||||
} else {
|
||||
path.addLine(to: point)
|
||||
}
|
||||
}
|
||||
path.closeSubpath()
|
||||
return path
|
||||
}
|
||||
|
||||
static func radialEnvelopeMagnitude(progress: Double, samples: [Double]) -> 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<Double> = 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)
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user