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:
Colin Johnson
2026-07-18 14:30:10 -04:00
committed by GitHub
parent 3572fba7f9
commit 1ca6b6ff67
81 changed files with 11763 additions and 11588 deletions
+61 -318
View File
@@ -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",
"Talku 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",
"บันทึกเสียง"
"ข้อความเสียง"
]
},
{
File diff suppressed because it is too large Load Diff
+190 -250
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 dun 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 na 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 dexé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": "Lapprobation 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 dapprobation"
},
{
"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 dautorisations…"
},
{
"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 dapprobation"
},
{
"id": "native.apple.55db7317a2542e51",
"source": "Approval sent",
"translated": "Approbation envoyée"
},
{
"id": "native.apple.06330f62ab254a0e",
"source": "Could not request approval",
"translated": "Impossible de demander lapprobation"
},
{
"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 dune nouvelle demande dassociation à 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 lapprobation 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 dapprobation"
},
{
"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 lopé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
View File
@@ -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
View File
@@ -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
View File
@@ -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 letichetta di stato del gateway"
},
{
"id": "native.apple.5fcf9d462463dd27",
"source": "Shows the full gateway status label",
"translated": "Mostra letichetta 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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 / Gatewayi 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": "Talku etkinleştir"
},
{
"id": "native.apple.abbfc1ba85a230b4",
"source": "Sending a new pairing request to your gateway...",
"translated": "Gatewayinize 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 gatewayinizde 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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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,
])
}
File diff suppressed because it is too large Load Diff
+279 -65
View File
@@ -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
}
-484
View File
@@ -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
}
}
+275 -50
View File
@@ -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)
}
+16
View File
@@ -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
View File
@@ -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
}
+4 -13
View File
@@ -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
}
}
+196 -99
View File
@@ -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 its 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(
+37 -6
View File
@@ -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)
+6 -29
View File
@@ -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"))
+91 -23
View File
@@ -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,
],
],
]
-145
View File
@@ -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)
}
}
+44 -69
View File
@@ -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)
+3
View File
@@ -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+(kxt)²))^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()
-8
View File
@@ -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),