From 45f6543c9ace017a7eb9ec2292a7bd2afd666095 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 18 Jul 2026 17:21:03 -0700 Subject: [PATCH] =?UTF-8?q?feat(macos):=20Quick=20Chat=20power=20features?= =?UTF-8?q?=20=E2=80=94=20voice=20dictation,=20paste-to-app,=20model/reaso?= =?UTF-8?q?ning=20control=20(#110994)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(macos): add Quick Chat power features * docs(macos): document Quick Chat power features * chore(i18n): refresh native locale artifacts for Quick Chat power features * fix(macos): update DEBUG test helper sendProvider to the reasoning-threaded arity --- apps/.i18n/native-source.json | 202 +++++++- apps/.i18n/native/ar.json | 105 ++++ apps/.i18n/native/de.json | 105 ++++ apps/.i18n/native/es.json | 105 ++++ apps/.i18n/native/fa.json | 105 ++++ apps/.i18n/native/fr.json | 105 ++++ apps/.i18n/native/hi.json | 105 ++++ apps/.i18n/native/id.json | 105 ++++ apps/.i18n/native/it.json | 105 ++++ apps/.i18n/native/ja-JP.json | 105 ++++ apps/.i18n/native/ko.json | 105 ++++ apps/.i18n/native/nl.json | 105 ++++ apps/.i18n/native/pl.json | 105 ++++ apps/.i18n/native/pt-BR.json | 105 ++++ apps/.i18n/native/ru.json | 105 ++++ apps/.i18n/native/sv.json | 105 ++++ apps/.i18n/native/th.json | 105 ++++ apps/.i18n/native/tr.json | 105 ++++ apps/.i18n/native/uk.json | 105 ++++ apps/.i18n/native/vi.json | 105 ++++ apps/.i18n/native/zh-CN.json | 105 ++++ apps/.i18n/native/zh-TW.json | 105 ++++ .../QuickChatController+Testing.swift | 2 +- .../OpenClaw/QuickChatController.swift | 249 ++++++++++ .../Sources/OpenClaw/QuickChatDictation.swift | 181 +++++++ .../Sources/OpenClaw/QuickChatModel.swift | 446 ++++++++++++++++- .../OpenClaw/QuickChatModelControls.swift | 163 ++++++ .../Sources/OpenClaw/QuickChatModelMenu.swift | 109 ++++ .../Sources/OpenClaw/QuickChatPaste.swift | 68 +++ .../OpenClaw/QuickChatReplyBinding.swift | 16 + .../Sources/OpenClaw/QuickChatTextView.swift | 25 +- .../Sources/OpenClaw/QuickChatView.swift | 103 +++- .../QuickChatControllerTests.swift | 24 +- .../QuickChatModelTests.swift | 75 ++- .../QuickChatPowerFeaturesTests.swift | 470 ++++++++++++++++++ .../QuickChatViewSmokeTests.swift | 10 +- .../ChatMessageVisibleText.swift | 4 +- .../OpenClawChatUI/ChatModelPickerStore.swift | 2 +- docs/platforms/mac/webchat.md | 6 + 39 files changed, 4302 insertions(+), 58 deletions(-) create mode 100644 apps/macos/Sources/OpenClaw/QuickChatDictation.swift create mode 100644 apps/macos/Sources/OpenClaw/QuickChatModelControls.swift create mode 100644 apps/macos/Sources/OpenClaw/QuickChatModelMenu.swift create mode 100644 apps/macos/Sources/OpenClaw/QuickChatPaste.swift create mode 100644 apps/macos/Tests/OpenClawIPCTests/QuickChatPowerFeaturesTests.swift diff --git a/apps/.i18n/native-source.json b/apps/.i18n/native-source.json index c22915e69fa..9d4514d7bcf 100644 --- a/apps/.i18n/native-source.json +++ b/apps/.i18n/native-source.json @@ -33787,7 +33787,63 @@ }, { "kind": "ui-localized-call", - "line": 616, + "line": 484, + "path": "apps/macos/Sources/OpenClaw/QuickChatController.swift", + "source": "Microphone and speech recognition permission required.", + "surface": "apple", + "id": "native.apple.237f02309f0b9aeb" + }, + { + "kind": "ui-localized-call", + "line": 512, + "path": "apps/macos/Sources/OpenClaw/QuickChatController.swift", + "source": "Dictation stopped because speech recognition failed.", + "surface": "apple", + "id": "native.apple.6f2679049010dae7" + }, + { + "kind": "ui-localized-call", + "line": 517, + "path": "apps/macos/Sources/OpenClaw/QuickChatController.swift", + "source": "Couldn't start dictation.", + "surface": "apple", + "id": "native.apple.fd0469907ed8a50c" + }, + { + "kind": "ui-localized-call", + "line": 546, + "path": "apps/macos/Sources/OpenClaw/QuickChatController.swift", + "source": "No app is available to paste into.", + "surface": "apple", + "id": "native.apple.ec789bf13bcffb61" + }, + { + "kind": "ui-localized-call", + "line": 553, + "path": "apps/macos/Sources/OpenClaw/QuickChatController.swift", + "source": "Choose another app before pasting.", + "surface": "apple", + "id": "native.apple.bd2b8e99eee923b4" + }, + { + "kind": "ui-localized-call", + "line": 568, + "path": "apps/macos/Sources/OpenClaw/QuickChatController.swift", + "source": "Accessibility permission is required to paste.", + "surface": "apple", + "id": "native.apple.f7aeeb401f9a4cd1" + }, + { + "kind": "ui-localized-call", + "line": 594, + "path": "apps/macos/Sources/OpenClaw/QuickChatController.swift", + "source": "Couldn't paste the reply.", + "surface": "apple", + "id": "native.apple.14e9c9f41a9523c0" + }, + { + "kind": "ui-localized-call", + "line": 865, "path": "apps/macos/Sources/OpenClaw/QuickChatController.swift", "source": "Capture Window…", "surface": "apple", @@ -33795,7 +33851,7 @@ }, { "kind": "ui-localized-call", - "line": 617, + "line": 866, "path": "apps/macos/Sources/OpenClaw/QuickChatController.swift", "source": "Capture Area…", "surface": "apple", @@ -33907,7 +33963,7 @@ }, { "kind": "ui-localized-call", - "line": 154, + "line": 164, "path": "apps/macos/Sources/OpenClaw/QuickChatModel.swift", "source": "focused app", "surface": "apple", @@ -33915,7 +33971,7 @@ }, { "kind": "ui-localized-call", - "line": 466, + "line": 614, "path": "apps/macos/Sources/OpenClaw/QuickChatModel.swift", "source": "Couldn't capture that image.", "surface": "apple", @@ -33923,7 +33979,7 @@ }, { "kind": "ui-localized-call", - "line": 488, + "line": 636, "path": "apps/macos/Sources/OpenClaw/QuickChatModel.swift", "source": "Screenshot: \\(label)", "surface": "apple", @@ -33931,12 +33987,84 @@ }, { "kind": "ui-localized-call", - "line": 549, + "line": 697, "path": "apps/macos/Sources/OpenClaw/QuickChatModel.swift", "source": "Context from \\(context.appName) — \\(context.windowTitle)", "surface": "apple", "id": "native.apple.72e5bffb7ee89f19" }, + { + "kind": "ui-localized-call", + "line": 1030, + "path": "apps/macos/Sources/OpenClaw/QuickChatModel.swift", + "source": "Auto", + "surface": "apple", + "id": "native.apple.d82159d749697338" + }, + { + "kind": "ui-localized-call", + "line": 1038, + "path": "apps/macos/Sources/OpenClaw/QuickChatModel.swift", + "source": "\\(model) · \\(thinking)", + "surface": "apple", + "id": "native.apple.4e02ff977e48dfcb" + }, + { + "kind": "ui-localized-call", + "line": 1062, + "path": "apps/macos/Sources/OpenClaw/QuickChatModel.swift", + "source": "Selected reasoning is unavailable for this target.", + "surface": "apple", + "id": "native.apple.07bd035ae7e799c5" + }, + { + "kind": "ui-localized-call", + "line": 1195, + "path": "apps/macos/Sources/OpenClaw/QuickChatModel.swift", + "source": "Couldn't load model settings.", + "surface": "apple", + "id": "native.apple.2eb721ed8bffe9aa" + }, + { + "kind": "ui-localized-call", + "line": 1206, + "path": "apps/macos/Sources/OpenClaw/QuickChatModel.swift", + "source": "Couldn't change the model.", + "surface": "apple", + "id": "native.apple.ef6f4c54f9cfac08" + }, + { + "kind": "ui-localized-call", + "line": 44, + "path": "apps/macos/Sources/OpenClaw/QuickChatModelMenu.swift", + "source": "Model", + "surface": "apple", + "id": "native.apple.82b81bd0670b0cef" + }, + { + "kind": "ui-localized-call", + "line": 51, + "path": "apps/macos/Sources/OpenClaw/QuickChatModelMenu.swift", + "source": "Session default", + "surface": "apple", + "id": "native.apple.2eb4c724364c7583" + }, + { + "kind": "ui-localized-call", + "line": 80, + "path": "apps/macos/Sources/OpenClaw/QuickChatModelMenu.swift", + "source": "Reasoning", + "surface": "apple", + "id": "native.apple.9f1c4db792611774" + }, + { + "kind": "ui-localized-call", + "line": 86, + "path": "apps/macos/Sources/OpenClaw/QuickChatModelMenu.swift", + "source": "Auto", + "surface": "apple", + "id": "native.apple.25a01f61b4e33ece" + }, { "kind": "ui-localized-call", "line": 22, @@ -33947,7 +34075,7 @@ }, { "kind": "ui-call", - "line": 75, + "line": 80, "path": "apps/macos/Sources/OpenClaw/QuickChatView.swift", "source": "Reply in \\(override.displayName)", "surface": "apple", @@ -33955,7 +34083,7 @@ }, { "kind": "ui-call", - "line": 77, + "line": 82, "path": "apps/macos/Sources/OpenClaw/QuickChatView.swift", "source": "Message \\(self.model.agentDisplay.name)", "surface": "apple", @@ -33963,7 +34091,7 @@ }, { "kind": "ui-modifier", - "line": 116, + "line": 130, "path": "apps/macos/Sources/OpenClaw/QuickChatView.swift", "source": "Continue a recent conversation", "surface": "apple", @@ -33971,7 +34099,7 @@ }, { "kind": "ui-localized-call", - "line": 135, + "line": 170, "path": "apps/macos/Sources/OpenClaw/QuickChatView.swift", "source": "Attach text from \\(self.model.frontmostAppName)", "surface": "apple", @@ -33979,7 +34107,7 @@ }, { "kind": "ui-modifier", - "line": 146, + "line": 181, "path": "apps/macos/Sources/OpenClaw/QuickChatView.swift", "source": "Capture a screenshot", "surface": "apple", @@ -33987,15 +34115,31 @@ }, { "kind": "ui-modifier", - "line": 170, + "line": 205, "path": "apps/macos/Sources/OpenClaw/QuickChatView.swift", "source": "Send message", "surface": "apple", "id": "native.apple.136cef9f750d3803" }, + { + "kind": "ui-modifier", + "line": 247, + "path": "apps/macos/Sources/OpenClaw/QuickChatView.swift", + "source": "Choose model and reasoning", + "surface": "apple", + "id": "native.apple.b520a305b56a6387" + }, + { + "kind": "ui-modifier", + "line": 248, + "path": "apps/macos/Sources/OpenClaw/QuickChatView.swift", + "source": "Model and reasoning", + "surface": "apple", + "id": "native.apple.1dd50e6126da7179" + }, { "kind": "ui-call", - "line": 228, + "line": 289, "path": "apps/macos/Sources/OpenClaw/QuickChatView.swift", "source": "Needs additional permissions", "surface": "apple", @@ -34003,7 +34147,7 @@ }, { "kind": "ui-call", - "line": 235, + "line": 296, "path": "apps/macos/Sources/OpenClaw/QuickChatView.swift", "source": "Grant", "surface": "apple", @@ -34011,7 +34155,7 @@ }, { "kind": "ui-call", - "line": 241, + "line": 302, "path": "apps/macos/Sources/OpenClaw/QuickChatView.swift", "source": "Not now", "surface": "apple", @@ -34019,7 +34163,7 @@ }, { "kind": "ui-call", - "line": 255, + "line": 316, "path": "apps/macos/Sources/OpenClaw/QuickChatView.swift", "source": "\\(context.appName) — \\(context.windowTitle) (\\(context.characterCount) chars)", "surface": "apple", @@ -34027,12 +34171,36 @@ }, { "kind": "ui-modifier", - "line": 264, + "line": 325, "path": "apps/macos/Sources/OpenClaw/QuickChatView.swift", "source": "Remove attached text context", "surface": "apple", "id": "native.apple.66ec93e9eefd9bd7" }, + { + "kind": "ui-call", + "line": 364, + "path": "apps/macos/Sources/OpenClaw/QuickChatView.swift", + "source": "Paste to \\(self.model.frontmostAppName)", + "surface": "apple", + "id": "native.apple.00b46fe92d9bb1d4" + }, + { + "kind": "ui-localized-call", + "line": 417, + "path": "apps/macos/Sources/OpenClaw/QuickChatView.swift", + "source": "Stop dictation", + "surface": "apple", + "id": "native.apple.d2aa2e60ec2f62d8" + }, + { + "kind": "ui-localized-call", + "line": 418, + "path": "apps/macos/Sources/OpenClaw/QuickChatView.swift", + "source": "Start dictation", + "surface": "apple", + "id": "native.apple.8c4a27a4361d0572" + }, { "kind": "ui-localized-call", "line": 221, diff --git a/apps/.i18n/native/ar.json b/apps/.i18n/native/ar.json index 3436ebc9765..937fc64936a 100644 --- a/apps/.i18n/native/ar.json +++ b/apps/.i18n/native/ar.json @@ -21118,6 +21118,41 @@ "source": "Continue", "translated": "متابعة" }, + { + "id": "native.apple.237f02309f0b9aeb", + "source": "Microphone and speech recognition permission required.", + "translated": "يلزم منح إذن استخدام الميكروفون والتعرّف على الكلام." + }, + { + "id": "native.apple.6f2679049010dae7", + "source": "Dictation stopped because speech recognition failed.", + "translated": "توقف الإملاء بسبب فشل التعرّف على الكلام." + }, + { + "id": "native.apple.fd0469907ed8a50c", + "source": "Couldn't start dictation.", + "translated": "تعذر بدء الإملاء." + }, + { + "id": "native.apple.ec789bf13bcffb61", + "source": "No app is available to paste into.", + "translated": "لا يوجد تطبيق متاح للصق فيه." + }, + { + "id": "native.apple.bd2b8e99eee923b4", + "source": "Choose another app before pasting.", + "translated": "اختر تطبيقًا آخر قبل اللصق." + }, + { + "id": "native.apple.f7aeeb401f9a4cd1", + "source": "Accessibility permission is required to paste.", + "translated": "يلزم منح إذن تسهيلات الاستخدام للصق." + }, + { + "id": "native.apple.14e9c9f41a9523c0", + "source": "Couldn't paste the reply.", + "translated": "تعذر لصق الرد." + }, { "id": "native.apple.0a4fcdf09c5e7b3f", "source": "Capture Window…", @@ -21213,6 +21248,51 @@ "source": "Context from \\(context.appName) — \\(context.windowTitle)", "translated": "سياق من \\(context.appName) — \\(context.windowTitle)" }, + { + "id": "native.apple.d82159d749697338", + "source": "Auto", + "translated": "تلقائي" + }, + { + "id": "native.apple.4e02ff977e48dfcb", + "source": "\\(model) · \\(thinking)", + "translated": "\\(model) · \\(thinking)" + }, + { + "id": "native.apple.07bd035ae7e799c5", + "source": "Selected reasoning is unavailable for this target.", + "translated": "الاستدلال المحدد غير متاح لهذه الوجهة." + }, + { + "id": "native.apple.2eb721ed8bffe9aa", + "source": "Couldn't load model settings.", + "translated": "تعذر تحميل إعدادات النموذج." + }, + { + "id": "native.apple.ef6f4c54f9cfac08", + "source": "Couldn't change the model.", + "translated": "تعذر تغيير النموذج." + }, + { + "id": "native.apple.82b81bd0670b0cef", + "source": "Model", + "translated": "النموذج" + }, + { + "id": "native.apple.2eb4c724364c7583", + "source": "Session default", + "translated": "الإعداد الافتراضي للجلسة" + }, + { + "id": "native.apple.9f1c4db792611774", + "source": "Reasoning", + "translated": "الاستدلال" + }, + { + "id": "native.apple.25a01f61b4e33ece", + "source": "Auto", + "translated": "تلقائي" + }, { "id": "native.apple.b5524e37a230e571", "source": "New message to \\(agentName)", @@ -21248,6 +21328,16 @@ "source": "Send message", "translated": "إرسال رسالة" }, + { + "id": "native.apple.b520a305b56a6387", + "source": "Choose model and reasoning", + "translated": "اختر النموذج والاستدلال" + }, + { + "id": "native.apple.1dd50e6126da7179", + "source": "Model and reasoning", + "translated": "النموذج والاستدلال" + }, { "id": "native.apple.024725d220e79697", "source": "Needs additional permissions", @@ -21273,6 +21363,21 @@ "source": "Remove attached text context", "translated": "إزالة سياق النص المرفق" }, + { + "id": "native.apple.00b46fe92d9bb1d4", + "source": "Paste to \\(self.model.frontmostAppName)", + "translated": "لصق في \\(self.model.frontmostAppName)" + }, + { + "id": "native.apple.d2aa2e60ec2f62d8", + "source": "Stop dictation", + "translated": "إيقاف الإملاء" + }, + { + "id": "native.apple.8c4a27a4361d0572", + "source": "Start dictation", + "translated": "بدء الإملاء" + }, { "id": "native.apple.88d0123c86b06144", "source": "Allow OpenClaw to capture windows", diff --git a/apps/.i18n/native/de.json b/apps/.i18n/native/de.json index d9b26bd2a44..68ddd8889f0 100644 --- a/apps/.i18n/native/de.json +++ b/apps/.i18n/native/de.json @@ -21118,6 +21118,41 @@ "source": "Continue", "translated": "Fortfahren" }, + { + "id": "native.apple.237f02309f0b9aeb", + "source": "Microphone and speech recognition permission required.", + "translated": "Berechtigung für Mikrofon und Spracherkennung erforderlich." + }, + { + "id": "native.apple.6f2679049010dae7", + "source": "Dictation stopped because speech recognition failed.", + "translated": "Das Diktat wurde beendet, weil die Spracherkennung fehlgeschlagen ist." + }, + { + "id": "native.apple.fd0469907ed8a50c", + "source": "Couldn't start dictation.", + "translated": "Diktat konnte nicht gestartet werden." + }, + { + "id": "native.apple.ec789bf13bcffb61", + "source": "No app is available to paste into.", + "translated": "Es ist keine App zum Einfügen verfügbar." + }, + { + "id": "native.apple.bd2b8e99eee923b4", + "source": "Choose another app before pasting.", + "translated": "Wähle vor dem Einfügen eine andere App aus." + }, + { + "id": "native.apple.f7aeeb401f9a4cd1", + "source": "Accessibility permission is required to paste.", + "translated": "Zum Einfügen ist die Bedienungshilfen-Berechtigung erforderlich." + }, + { + "id": "native.apple.14e9c9f41a9523c0", + "source": "Couldn't paste the reply.", + "translated": "Die Antwort konnte nicht eingefügt werden." + }, { "id": "native.apple.0a4fcdf09c5e7b3f", "source": "Capture Window…", @@ -21213,6 +21248,51 @@ "source": "Context from \\(context.appName) — \\(context.windowTitle)", "translated": "Kontext aus \\(context.appName) — \\(context.windowTitle)" }, + { + "id": "native.apple.d82159d749697338", + "source": "Auto", + "translated": "Automatisch" + }, + { + "id": "native.apple.4e02ff977e48dfcb", + "source": "\\(model) · \\(thinking)", + "translated": "\\(model) · \\(thinking)" + }, + { + "id": "native.apple.07bd035ae7e799c5", + "source": "Selected reasoning is unavailable for this target.", + "translated": "Die ausgewählte Schlussfolgerung ist für dieses Ziel nicht verfügbar." + }, + { + "id": "native.apple.2eb721ed8bffe9aa", + "source": "Couldn't load model settings.", + "translated": "Die Modelleinstellungen konnten nicht geladen werden." + }, + { + "id": "native.apple.ef6f4c54f9cfac08", + "source": "Couldn't change the model.", + "translated": "Das Modell konnte nicht geändert werden." + }, + { + "id": "native.apple.82b81bd0670b0cef", + "source": "Model", + "translated": "Modell" + }, + { + "id": "native.apple.2eb4c724364c7583", + "source": "Session default", + "translated": "Sitzungsstandard" + }, + { + "id": "native.apple.9f1c4db792611774", + "source": "Reasoning", + "translated": "Schlussfolgerung" + }, + { + "id": "native.apple.25a01f61b4e33ece", + "source": "Auto", + "translated": "Automatisch" + }, { "id": "native.apple.b5524e37a230e571", "source": "New message to \\(agentName)", @@ -21248,6 +21328,16 @@ "source": "Send message", "translated": "Nachricht senden" }, + { + "id": "native.apple.b520a305b56a6387", + "source": "Choose model and reasoning", + "translated": "Modell und Schlussfolgerung auswählen" + }, + { + "id": "native.apple.1dd50e6126da7179", + "source": "Model and reasoning", + "translated": "Modell und Schlussfolgerung" + }, { "id": "native.apple.024725d220e79697", "source": "Needs additional permissions", @@ -21273,6 +21363,21 @@ "source": "Remove attached text context", "translated": "Angehängten Textkontext entfernen" }, + { + "id": "native.apple.00b46fe92d9bb1d4", + "source": "Paste to \\(self.model.frontmostAppName)", + "translated": "In \\(self.model.frontmostAppName) einfügen" + }, + { + "id": "native.apple.d2aa2e60ec2f62d8", + "source": "Stop dictation", + "translated": "Diktat beenden" + }, + { + "id": "native.apple.8c4a27a4361d0572", + "source": "Start dictation", + "translated": "Diktat starten" + }, { "id": "native.apple.88d0123c86b06144", "source": "Allow OpenClaw to capture windows", diff --git a/apps/.i18n/native/es.json b/apps/.i18n/native/es.json index d50b93189ce..de66dfcde49 100644 --- a/apps/.i18n/native/es.json +++ b/apps/.i18n/native/es.json @@ -21118,6 +21118,41 @@ "source": "Continue", "translated": "Continuar" }, + { + "id": "native.apple.237f02309f0b9aeb", + "source": "Microphone and speech recognition permission required.", + "translated": "Se requieren permisos para usar el micrófono y el reconocimiento de voz." + }, + { + "id": "native.apple.6f2679049010dae7", + "source": "Dictation stopped because speech recognition failed.", + "translated": "El dictado se detuvo porque falló el reconocimiento de voz." + }, + { + "id": "native.apple.fd0469907ed8a50c", + "source": "Couldn't start dictation.", + "translated": "No se pudo iniciar el dictado." + }, + { + "id": "native.apple.ec789bf13bcffb61", + "source": "No app is available to paste into.", + "translated": "No hay ninguna app disponible en la que pegar." + }, + { + "id": "native.apple.bd2b8e99eee923b4", + "source": "Choose another app before pasting.", + "translated": "Elige otra app antes de pegar." + }, + { + "id": "native.apple.f7aeeb401f9a4cd1", + "source": "Accessibility permission is required to paste.", + "translated": "Se requiere el permiso de accesibilidad para pegar." + }, + { + "id": "native.apple.14e9c9f41a9523c0", + "source": "Couldn't paste the reply.", + "translated": "No se pudo pegar la respuesta." + }, { "id": "native.apple.0a4fcdf09c5e7b3f", "source": "Capture Window…", @@ -21213,6 +21248,51 @@ "source": "Context from \\(context.appName) — \\(context.windowTitle)", "translated": "Contexto de \\(context.appName) — \\(context.windowTitle)" }, + { + "id": "native.apple.d82159d749697338", + "source": "Auto", + "translated": "Automático" + }, + { + "id": "native.apple.4e02ff977e48dfcb", + "source": "\\(model) · \\(thinking)", + "translated": "\\(model) · \\(thinking)" + }, + { + "id": "native.apple.07bd035ae7e799c5", + "source": "Selected reasoning is unavailable for this target.", + "translated": "El razonamiento seleccionado no está disponible para este destino." + }, + { + "id": "native.apple.2eb721ed8bffe9aa", + "source": "Couldn't load model settings.", + "translated": "No se pudo cargar la configuración del modelo." + }, + { + "id": "native.apple.ef6f4c54f9cfac08", + "source": "Couldn't change the model.", + "translated": "No se pudo cambiar el modelo." + }, + { + "id": "native.apple.82b81bd0670b0cef", + "source": "Model", + "translated": "Modelo" + }, + { + "id": "native.apple.2eb4c724364c7583", + "source": "Session default", + "translated": "Predeterminado de la sesión" + }, + { + "id": "native.apple.9f1c4db792611774", + "source": "Reasoning", + "translated": "Razonamiento" + }, + { + "id": "native.apple.25a01f61b4e33ece", + "source": "Auto", + "translated": "Automático" + }, { "id": "native.apple.b5524e37a230e571", "source": "New message to \\(agentName)", @@ -21248,6 +21328,16 @@ "source": "Send message", "translated": "Enviar mensaje" }, + { + "id": "native.apple.b520a305b56a6387", + "source": "Choose model and reasoning", + "translated": "Elegir modelo y razonamiento" + }, + { + "id": "native.apple.1dd50e6126da7179", + "source": "Model and reasoning", + "translated": "Modelo y razonamiento" + }, { "id": "native.apple.024725d220e79697", "source": "Needs additional permissions", @@ -21273,6 +21363,21 @@ "source": "Remove attached text context", "translated": "Eliminar el contexto de texto adjunto" }, + { + "id": "native.apple.00b46fe92d9bb1d4", + "source": "Paste to \\(self.model.frontmostAppName)", + "translated": "Pegar en \\(self.model.frontmostAppName)" + }, + { + "id": "native.apple.d2aa2e60ec2f62d8", + "source": "Stop dictation", + "translated": "Detener dictado" + }, + { + "id": "native.apple.8c4a27a4361d0572", + "source": "Start dictation", + "translated": "Iniciar dictado" + }, { "id": "native.apple.88d0123c86b06144", "source": "Allow OpenClaw to capture windows", diff --git a/apps/.i18n/native/fa.json b/apps/.i18n/native/fa.json index bb9107bf1f4..d4786dd59f9 100644 --- a/apps/.i18n/native/fa.json +++ b/apps/.i18n/native/fa.json @@ -21118,6 +21118,41 @@ "source": "Continue", "translated": "ادامه" }, + { + "id": "native.apple.237f02309f0b9aeb", + "source": "Microphone and speech recognition permission required.", + "translated": "مجوز دسترسی به میکروفون و تشخیص گفتار لازم است." + }, + { + "id": "native.apple.6f2679049010dae7", + "source": "Dictation stopped because speech recognition failed.", + "translated": "دیکته به‌دلیل ناموفق بودن تشخیص گفتار متوقف شد." + }, + { + "id": "native.apple.fd0469907ed8a50c", + "source": "Couldn't start dictation.", + "translated": "دیکته شروع نشد." + }, + { + "id": "native.apple.ec789bf13bcffb61", + "source": "No app is available to paste into.", + "translated": "هیچ برنامه‌ای برای جای‌گذاری در آن در دسترس نیست." + }, + { + "id": "native.apple.bd2b8e99eee923b4", + "source": "Choose another app before pasting.", + "translated": "پیش از جای‌گذاری، برنامه دیگری را انتخاب کنید." + }, + { + "id": "native.apple.f7aeeb401f9a4cd1", + "source": "Accessibility permission is required to paste.", + "translated": "برای جای‌گذاری، مجوز دسترس‌پذیری لازم است." + }, + { + "id": "native.apple.14e9c9f41a9523c0", + "source": "Couldn't paste the reply.", + "translated": "پاسخ جای‌گذاری نشد." + }, { "id": "native.apple.0a4fcdf09c5e7b3f", "source": "Capture Window…", @@ -21213,6 +21248,51 @@ "source": "Context from \\(context.appName) — \\(context.windowTitle)", "translated": "زمینه از \\(context.appName) — \\(context.windowTitle)" }, + { + "id": "native.apple.d82159d749697338", + "source": "Auto", + "translated": "خودکار" + }, + { + "id": "native.apple.4e02ff977e48dfcb", + "source": "\\(model) · \\(thinking)", + "translated": "\\(model) · \\(thinking)" + }, + { + "id": "native.apple.07bd035ae7e799c5", + "source": "Selected reasoning is unavailable for this target.", + "translated": "استدلال انتخاب‌شده برای این مقصد در دسترس نیست." + }, + { + "id": "native.apple.2eb721ed8bffe9aa", + "source": "Couldn't load model settings.", + "translated": "تنظیمات مدل بارگیری نشد." + }, + { + "id": "native.apple.ef6f4c54f9cfac08", + "source": "Couldn't change the model.", + "translated": "مدل تغییر نکرد." + }, + { + "id": "native.apple.82b81bd0670b0cef", + "source": "Model", + "translated": "مدل" + }, + { + "id": "native.apple.2eb4c724364c7583", + "source": "Session default", + "translated": "پیش‌فرض نشست" + }, + { + "id": "native.apple.9f1c4db792611774", + "source": "Reasoning", + "translated": "استدلال" + }, + { + "id": "native.apple.25a01f61b4e33ece", + "source": "Auto", + "translated": "خودکار" + }, { "id": "native.apple.b5524e37a230e571", "source": "New message to \\(agentName)", @@ -21248,6 +21328,16 @@ "source": "Send message", "translated": "ارسال پیام" }, + { + "id": "native.apple.b520a305b56a6387", + "source": "Choose model and reasoning", + "translated": "انتخاب مدل و استدلال" + }, + { + "id": "native.apple.1dd50e6126da7179", + "source": "Model and reasoning", + "translated": "مدل و استدلال" + }, { "id": "native.apple.024725d220e79697", "source": "Needs additional permissions", @@ -21273,6 +21363,21 @@ "source": "Remove attached text context", "translated": "حذف متن زمینهٔ پیوست‌شده" }, + { + "id": "native.apple.00b46fe92d9bb1d4", + "source": "Paste to \\(self.model.frontmostAppName)", + "translated": "جای‌گذاری در \\(self.model.frontmostAppName)" + }, + { + "id": "native.apple.d2aa2e60ec2f62d8", + "source": "Stop dictation", + "translated": "توقف دیکته" + }, + { + "id": "native.apple.8c4a27a4361d0572", + "source": "Start dictation", + "translated": "شروع دیکته" + }, { "id": "native.apple.88d0123c86b06144", "source": "Allow OpenClaw to capture windows", diff --git a/apps/.i18n/native/fr.json b/apps/.i18n/native/fr.json index b6c4d898bab..4ebdc3825c8 100644 --- a/apps/.i18n/native/fr.json +++ b/apps/.i18n/native/fr.json @@ -21118,6 +21118,41 @@ "source": "Continue", "translated": "Continuer" }, + { + "id": "native.apple.237f02309f0b9aeb", + "source": "Microphone and speech recognition permission required.", + "translated": "L’autorisation d’accéder au microphone et à la reconnaissance vocale est requise." + }, + { + "id": "native.apple.6f2679049010dae7", + "source": "Dictation stopped because speech recognition failed.", + "translated": "La dictée s’est arrêtée en raison de l’échec de la reconnaissance vocale." + }, + { + "id": "native.apple.fd0469907ed8a50c", + "source": "Couldn't start dictation.", + "translated": "Impossible de démarrer la dictée." + }, + { + "id": "native.apple.ec789bf13bcffb61", + "source": "No app is available to paste into.", + "translated": "Aucune app n’est disponible pour effectuer le collage." + }, + { + "id": "native.apple.bd2b8e99eee923b4", + "source": "Choose another app before pasting.", + "translated": "Choisissez une autre app avant de coller." + }, + { + "id": "native.apple.f7aeeb401f9a4cd1", + "source": "Accessibility permission is required to paste.", + "translated": "L’autorisation d’accessibilité est requise pour effectuer le collage." + }, + { + "id": "native.apple.14e9c9f41a9523c0", + "source": "Couldn't paste the reply.", + "translated": "Impossible de coller la réponse." + }, { "id": "native.apple.0a4fcdf09c5e7b3f", "source": "Capture Window…", @@ -21213,6 +21248,51 @@ "source": "Context from \\(context.appName) — \\(context.windowTitle)", "translated": "Contexte de \\(context.appName) — \\(context.windowTitle)" }, + { + "id": "native.apple.d82159d749697338", + "source": "Auto", + "translated": "Auto" + }, + { + "id": "native.apple.4e02ff977e48dfcb", + "source": "\\(model) · \\(thinking)", + "translated": "\\(model) · \\(thinking)" + }, + { + "id": "native.apple.07bd035ae7e799c5", + "source": "Selected reasoning is unavailable for this target.", + "translated": "Le raisonnement sélectionné n’est pas disponible pour cette cible." + }, + { + "id": "native.apple.2eb721ed8bffe9aa", + "source": "Couldn't load model settings.", + "translated": "Impossible de charger les réglages du modèle." + }, + { + "id": "native.apple.ef6f4c54f9cfac08", + "source": "Couldn't change the model.", + "translated": "Impossible de changer de modèle." + }, + { + "id": "native.apple.82b81bd0670b0cef", + "source": "Model", + "translated": "Modèle" + }, + { + "id": "native.apple.2eb4c724364c7583", + "source": "Session default", + "translated": "Valeur par défaut de la session" + }, + { + "id": "native.apple.9f1c4db792611774", + "source": "Reasoning", + "translated": "Raisonnement" + }, + { + "id": "native.apple.25a01f61b4e33ece", + "source": "Auto", + "translated": "Auto" + }, { "id": "native.apple.b5524e37a230e571", "source": "New message to \\(agentName)", @@ -21248,6 +21328,16 @@ "source": "Send message", "translated": "Envoyer le message" }, + { + "id": "native.apple.b520a305b56a6387", + "source": "Choose model and reasoning", + "translated": "Choisir le modèle et le raisonnement" + }, + { + "id": "native.apple.1dd50e6126da7179", + "source": "Model and reasoning", + "translated": "Modèle et raisonnement" + }, { "id": "native.apple.024725d220e79697", "source": "Needs additional permissions", @@ -21273,6 +21363,21 @@ "source": "Remove attached text context", "translated": "Supprimer le contexte textuel joint" }, + { + "id": "native.apple.00b46fe92d9bb1d4", + "source": "Paste to \\(self.model.frontmostAppName)", + "translated": "Coller dans \\(self.model.frontmostAppName)" + }, + { + "id": "native.apple.d2aa2e60ec2f62d8", + "source": "Stop dictation", + "translated": "Arrêter la dictée" + }, + { + "id": "native.apple.8c4a27a4361d0572", + "source": "Start dictation", + "translated": "Démarrer la dictée" + }, { "id": "native.apple.88d0123c86b06144", "source": "Allow OpenClaw to capture windows", diff --git a/apps/.i18n/native/hi.json b/apps/.i18n/native/hi.json index 01422ffd2b0..415f7180276 100644 --- a/apps/.i18n/native/hi.json +++ b/apps/.i18n/native/hi.json @@ -21118,6 +21118,41 @@ "source": "Continue", "translated": "जारी रखें" }, + { + "id": "native.apple.237f02309f0b9aeb", + "source": "Microphone and speech recognition permission required.", + "translated": "माइक्रोफ़ोन और वाक् पहचान की अनुमति आवश्यक है।" + }, + { + "id": "native.apple.6f2679049010dae7", + "source": "Dictation stopped because speech recognition failed.", + "translated": "वाक् पहचान विफल होने के कारण डिक्टेशन रुक गया।" + }, + { + "id": "native.apple.fd0469907ed8a50c", + "source": "Couldn't start dictation.", + "translated": "डिक्टेशन शुरू नहीं किया जा सका।" + }, + { + "id": "native.apple.ec789bf13bcffb61", + "source": "No app is available to paste into.", + "translated": "पेस्ट करने के लिए कोई ऐप उपलब्ध नहीं है।" + }, + { + "id": "native.apple.bd2b8e99eee923b4", + "source": "Choose another app before pasting.", + "translated": "पेस्ट करने से पहले कोई दूसरा ऐप चुनें।" + }, + { + "id": "native.apple.f7aeeb401f9a4cd1", + "source": "Accessibility permission is required to paste.", + "translated": "पेस्ट करने के लिए ऐक्सेसिबिलिटी की अनुमति आवश्यक है।" + }, + { + "id": "native.apple.14e9c9f41a9523c0", + "source": "Couldn't paste the reply.", + "translated": "जवाब पेस्ट नहीं किया जा सका।" + }, { "id": "native.apple.0a4fcdf09c5e7b3f", "source": "Capture Window…", @@ -21213,6 +21248,51 @@ "source": "Context from \\(context.appName) — \\(context.windowTitle)", "translated": "\\(context.appName) से संदर्भ — \\(context.windowTitle)" }, + { + "id": "native.apple.d82159d749697338", + "source": "Auto", + "translated": "स्वचालित" + }, + { + "id": "native.apple.4e02ff977e48dfcb", + "source": "\\(model) · \\(thinking)", + "translated": "\\(model) · \\(thinking)" + }, + { + "id": "native.apple.07bd035ae7e799c5", + "source": "Selected reasoning is unavailable for this target.", + "translated": "चुना गया तर्क इस लक्ष्य के लिए उपलब्ध नहीं है।" + }, + { + "id": "native.apple.2eb721ed8bffe9aa", + "source": "Couldn't load model settings.", + "translated": "मॉडल सेटिंग लोड नहीं की जा सकीं।" + }, + { + "id": "native.apple.ef6f4c54f9cfac08", + "source": "Couldn't change the model.", + "translated": "मॉडल बदला नहीं जा सका।" + }, + { + "id": "native.apple.82b81bd0670b0cef", + "source": "Model", + "translated": "मॉडल" + }, + { + "id": "native.apple.2eb4c724364c7583", + "source": "Session default", + "translated": "सत्र का डिफ़ॉल्ट" + }, + { + "id": "native.apple.9f1c4db792611774", + "source": "Reasoning", + "translated": "तर्क" + }, + { + "id": "native.apple.25a01f61b4e33ece", + "source": "Auto", + "translated": "स्वचालित" + }, { "id": "native.apple.b5524e37a230e571", "source": "New message to \\(agentName)", @@ -21248,6 +21328,16 @@ "source": "Send message", "translated": "संदेश भेजें" }, + { + "id": "native.apple.b520a305b56a6387", + "source": "Choose model and reasoning", + "translated": "मॉडल और तर्क चुनें" + }, + { + "id": "native.apple.1dd50e6126da7179", + "source": "Model and reasoning", + "translated": "मॉडल और तर्क" + }, { "id": "native.apple.024725d220e79697", "source": "Needs additional permissions", @@ -21273,6 +21363,21 @@ "source": "Remove attached text context", "translated": "संलग्न टेक्स्ट संदर्भ हटाएँ" }, + { + "id": "native.apple.00b46fe92d9bb1d4", + "source": "Paste to \\(self.model.frontmostAppName)", + "translated": "\\(self.model.frontmostAppName) में पेस्ट करें" + }, + { + "id": "native.apple.d2aa2e60ec2f62d8", + "source": "Stop dictation", + "translated": "डिक्टेशन रोकें" + }, + { + "id": "native.apple.8c4a27a4361d0572", + "source": "Start dictation", + "translated": "डिक्टेशन शुरू करें" + }, { "id": "native.apple.88d0123c86b06144", "source": "Allow OpenClaw to capture windows", diff --git a/apps/.i18n/native/id.json b/apps/.i18n/native/id.json index 739300c7e5f..e8e8e03b64e 100644 --- a/apps/.i18n/native/id.json +++ b/apps/.i18n/native/id.json @@ -21118,6 +21118,41 @@ "source": "Continue", "translated": "Lanjutkan" }, + { + "id": "native.apple.237f02309f0b9aeb", + "source": "Microphone and speech recognition permission required.", + "translated": "Izin mikrofon dan pengenalan ucapan diperlukan." + }, + { + "id": "native.apple.6f2679049010dae7", + "source": "Dictation stopped because speech recognition failed.", + "translated": "Dikte dihentikan karena pengenalan ucapan gagal." + }, + { + "id": "native.apple.fd0469907ed8a50c", + "source": "Couldn't start dictation.", + "translated": "Tidak dapat memulai dikte." + }, + { + "id": "native.apple.ec789bf13bcffb61", + "source": "No app is available to paste into.", + "translated": "Tidak ada aplikasi yang tersedia untuk menempelkan." + }, + { + "id": "native.apple.bd2b8e99eee923b4", + "source": "Choose another app before pasting.", + "translated": "Pilih aplikasi lain sebelum menempelkan." + }, + { + "id": "native.apple.f7aeeb401f9a4cd1", + "source": "Accessibility permission is required to paste.", + "translated": "Izin Aksesibilitas diperlukan untuk menempelkan." + }, + { + "id": "native.apple.14e9c9f41a9523c0", + "source": "Couldn't paste the reply.", + "translated": "Tidak dapat menempelkan balasan." + }, { "id": "native.apple.0a4fcdf09c5e7b3f", "source": "Capture Window…", @@ -21213,6 +21248,51 @@ "source": "Context from \\(context.appName) — \\(context.windowTitle)", "translated": "Konteks dari \\(context.appName) — \\(context.windowTitle)" }, + { + "id": "native.apple.d82159d749697338", + "source": "Auto", + "translated": "Otomatis" + }, + { + "id": "native.apple.4e02ff977e48dfcb", + "source": "\\(model) · \\(thinking)", + "translated": "\\(model) · \\(thinking)" + }, + { + "id": "native.apple.07bd035ae7e799c5", + "source": "Selected reasoning is unavailable for this target.", + "translated": "Penalaran yang dipilih tidak tersedia untuk target ini." + }, + { + "id": "native.apple.2eb721ed8bffe9aa", + "source": "Couldn't load model settings.", + "translated": "Tidak dapat memuat pengaturan model." + }, + { + "id": "native.apple.ef6f4c54f9cfac08", + "source": "Couldn't change the model.", + "translated": "Tidak dapat mengubah model." + }, + { + "id": "native.apple.82b81bd0670b0cef", + "source": "Model", + "translated": "Model" + }, + { + "id": "native.apple.2eb4c724364c7583", + "source": "Session default", + "translated": "Default sesi" + }, + { + "id": "native.apple.9f1c4db792611774", + "source": "Reasoning", + "translated": "Penalaran" + }, + { + "id": "native.apple.25a01f61b4e33ece", + "source": "Auto", + "translated": "Otomatis" + }, { "id": "native.apple.b5524e37a230e571", "source": "New message to \\(agentName)", @@ -21248,6 +21328,16 @@ "source": "Send message", "translated": "Kirim pesan" }, + { + "id": "native.apple.b520a305b56a6387", + "source": "Choose model and reasoning", + "translated": "Pilih model dan penalaran" + }, + { + "id": "native.apple.1dd50e6126da7179", + "source": "Model and reasoning", + "translated": "Model dan penalaran" + }, { "id": "native.apple.024725d220e79697", "source": "Needs additional permissions", @@ -21273,6 +21363,21 @@ "source": "Remove attached text context", "translated": "Hapus konteks teks terlampir" }, + { + "id": "native.apple.00b46fe92d9bb1d4", + "source": "Paste to \\(self.model.frontmostAppName)", + "translated": "Tempel ke \\(self.model.frontmostAppName)" + }, + { + "id": "native.apple.d2aa2e60ec2f62d8", + "source": "Stop dictation", + "translated": "Hentikan dikte" + }, + { + "id": "native.apple.8c4a27a4361d0572", + "source": "Start dictation", + "translated": "Mulai dikte" + }, { "id": "native.apple.88d0123c86b06144", "source": "Allow OpenClaw to capture windows", diff --git a/apps/.i18n/native/it.json b/apps/.i18n/native/it.json index b27fb13ff38..c4ebc9a1455 100644 --- a/apps/.i18n/native/it.json +++ b/apps/.i18n/native/it.json @@ -21118,6 +21118,41 @@ "source": "Continue", "translated": "Continua" }, + { + "id": "native.apple.237f02309f0b9aeb", + "source": "Microphone and speech recognition permission required.", + "translated": "È richiesta l'autorizzazione per il microfono e il riconoscimento vocale." + }, + { + "id": "native.apple.6f2679049010dae7", + "source": "Dictation stopped because speech recognition failed.", + "translated": "La dettatura è stata interrotta perché il riconoscimento vocale non è riuscito." + }, + { + "id": "native.apple.fd0469907ed8a50c", + "source": "Couldn't start dictation.", + "translated": "Impossibile avviare la dettatura." + }, + { + "id": "native.apple.ec789bf13bcffb61", + "source": "No app is available to paste into.", + "translated": "Nessuna app disponibile in cui incollare." + }, + { + "id": "native.apple.bd2b8e99eee923b4", + "source": "Choose another app before pasting.", + "translated": "Scegli un'altra app prima di incollare." + }, + { + "id": "native.apple.f7aeeb401f9a4cd1", + "source": "Accessibility permission is required to paste.", + "translated": "È richiesta l'autorizzazione per l'accessibilità per incollare." + }, + { + "id": "native.apple.14e9c9f41a9523c0", + "source": "Couldn't paste the reply.", + "translated": "Impossibile incollare la risposta." + }, { "id": "native.apple.0a4fcdf09c5e7b3f", "source": "Capture Window…", @@ -21213,6 +21248,51 @@ "source": "Context from \\(context.appName) — \\(context.windowTitle)", "translated": "Contesto da \\(context.appName) — \\(context.windowTitle)" }, + { + "id": "native.apple.d82159d749697338", + "source": "Auto", + "translated": "Auto" + }, + { + "id": "native.apple.4e02ff977e48dfcb", + "source": "\\(model) · \\(thinking)", + "translated": "\\(model) · \\(thinking)" + }, + { + "id": "native.apple.07bd035ae7e799c5", + "source": "Selected reasoning is unavailable for this target.", + "translated": "Il ragionamento selezionato non è disponibile per questa destinazione." + }, + { + "id": "native.apple.2eb721ed8bffe9aa", + "source": "Couldn't load model settings.", + "translated": "Impossibile caricare le impostazioni del modello." + }, + { + "id": "native.apple.ef6f4c54f9cfac08", + "source": "Couldn't change the model.", + "translated": "Impossibile cambiare il modello." + }, + { + "id": "native.apple.82b81bd0670b0cef", + "source": "Model", + "translated": "Modello" + }, + { + "id": "native.apple.2eb4c724364c7583", + "source": "Session default", + "translated": "Predefinito della sessione" + }, + { + "id": "native.apple.9f1c4db792611774", + "source": "Reasoning", + "translated": "Ragionamento" + }, + { + "id": "native.apple.25a01f61b4e33ece", + "source": "Auto", + "translated": "Auto" + }, { "id": "native.apple.b5524e37a230e571", "source": "New message to \\(agentName)", @@ -21248,6 +21328,16 @@ "source": "Send message", "translated": "Invia messaggio" }, + { + "id": "native.apple.b520a305b56a6387", + "source": "Choose model and reasoning", + "translated": "Scegli modello e ragionamento" + }, + { + "id": "native.apple.1dd50e6126da7179", + "source": "Model and reasoning", + "translated": "Modello e ragionamento" + }, { "id": "native.apple.024725d220e79697", "source": "Needs additional permissions", @@ -21273,6 +21363,21 @@ "source": "Remove attached text context", "translated": "Rimuovi il contesto testuale allegato" }, + { + "id": "native.apple.00b46fe92d9bb1d4", + "source": "Paste to \\(self.model.frontmostAppName)", + "translated": "Incolla in \\(self.model.frontmostAppName)" + }, + { + "id": "native.apple.d2aa2e60ec2f62d8", + "source": "Stop dictation", + "translated": "Interrompi dettatura" + }, + { + "id": "native.apple.8c4a27a4361d0572", + "source": "Start dictation", + "translated": "Avvia dettatura" + }, { "id": "native.apple.88d0123c86b06144", "source": "Allow OpenClaw to capture windows", diff --git a/apps/.i18n/native/ja-JP.json b/apps/.i18n/native/ja-JP.json index 2424bd84cd6..ba97a04b3f5 100644 --- a/apps/.i18n/native/ja-JP.json +++ b/apps/.i18n/native/ja-JP.json @@ -21118,6 +21118,41 @@ "source": "Continue", "translated": "続ける" }, + { + "id": "native.apple.237f02309f0b9aeb", + "source": "Microphone and speech recognition permission required.", + "translated": "マイクと音声認識の権限が必要です。" + }, + { + "id": "native.apple.6f2679049010dae7", + "source": "Dictation stopped because speech recognition failed.", + "translated": "音声認識に失敗したため、音声入力を停止しました。" + }, + { + "id": "native.apple.fd0469907ed8a50c", + "source": "Couldn't start dictation.", + "translated": "音声入力を開始できませんでした。" + }, + { + "id": "native.apple.ec789bf13bcffb61", + "source": "No app is available to paste into.", + "translated": "貼り付け先として利用できるアプリがありません。" + }, + { + "id": "native.apple.bd2b8e99eee923b4", + "source": "Choose another app before pasting.", + "translated": "貼り付ける前に別のアプリを選択してください。" + }, + { + "id": "native.apple.f7aeeb401f9a4cd1", + "source": "Accessibility permission is required to paste.", + "translated": "貼り付けるにはアクセシビリティの権限が必要です。" + }, + { + "id": "native.apple.14e9c9f41a9523c0", + "source": "Couldn't paste the reply.", + "translated": "返信を貼り付けられませんでした。" + }, { "id": "native.apple.0a4fcdf09c5e7b3f", "source": "Capture Window…", @@ -21213,6 +21248,51 @@ "source": "Context from \\(context.appName) — \\(context.windowTitle)", "translated": "\\(context.appName)からのコンテキスト — \\(context.windowTitle)" }, + { + "id": "native.apple.d82159d749697338", + "source": "Auto", + "translated": "自動" + }, + { + "id": "native.apple.4e02ff977e48dfcb", + "source": "\\(model) · \\(thinking)", + "translated": "\\(model) · \\(thinking)" + }, + { + "id": "native.apple.07bd035ae7e799c5", + "source": "Selected reasoning is unavailable for this target.", + "translated": "選択した推論はこの対象では利用できません。" + }, + { + "id": "native.apple.2eb721ed8bffe9aa", + "source": "Couldn't load model settings.", + "translated": "モデル設定を読み込めませんでした。" + }, + { + "id": "native.apple.ef6f4c54f9cfac08", + "source": "Couldn't change the model.", + "translated": "モデルを変更できませんでした。" + }, + { + "id": "native.apple.82b81bd0670b0cef", + "source": "Model", + "translated": "モデル" + }, + { + "id": "native.apple.2eb4c724364c7583", + "source": "Session default", + "translated": "セッションのデフォルト" + }, + { + "id": "native.apple.9f1c4db792611774", + "source": "Reasoning", + "translated": "推論" + }, + { + "id": "native.apple.25a01f61b4e33ece", + "source": "Auto", + "translated": "自動" + }, { "id": "native.apple.b5524e37a230e571", "source": "New message to \\(agentName)", @@ -21248,6 +21328,16 @@ "source": "Send message", "translated": "メッセージを送信" }, + { + "id": "native.apple.b520a305b56a6387", + "source": "Choose model and reasoning", + "translated": "モデルと推論を選択" + }, + { + "id": "native.apple.1dd50e6126da7179", + "source": "Model and reasoning", + "translated": "モデルと推論" + }, { "id": "native.apple.024725d220e79697", "source": "Needs additional permissions", @@ -21273,6 +21363,21 @@ "source": "Remove attached text context", "translated": "添付されたテキストコンテキストを削除" }, + { + "id": "native.apple.00b46fe92d9bb1d4", + "source": "Paste to \\(self.model.frontmostAppName)", + "translated": "\\(self.model.frontmostAppName)に貼り付け" + }, + { + "id": "native.apple.d2aa2e60ec2f62d8", + "source": "Stop dictation", + "translated": "音声入力を停止" + }, + { + "id": "native.apple.8c4a27a4361d0572", + "source": "Start dictation", + "translated": "音声入力を開始" + }, { "id": "native.apple.88d0123c86b06144", "source": "Allow OpenClaw to capture windows", diff --git a/apps/.i18n/native/ko.json b/apps/.i18n/native/ko.json index 3245e106c73..3b30df929c5 100644 --- a/apps/.i18n/native/ko.json +++ b/apps/.i18n/native/ko.json @@ -21118,6 +21118,41 @@ "source": "Continue", "translated": "계속" }, + { + "id": "native.apple.237f02309f0b9aeb", + "source": "Microphone and speech recognition permission required.", + "translated": "마이크 및 음성 인식 권한이 필요합니다." + }, + { + "id": "native.apple.6f2679049010dae7", + "source": "Dictation stopped because speech recognition failed.", + "translated": "음성 인식에 실패하여 받아쓰기가 중지되었습니다." + }, + { + "id": "native.apple.fd0469907ed8a50c", + "source": "Couldn't start dictation.", + "translated": "받아쓰기를 시작할 수 없습니다." + }, + { + "id": "native.apple.ec789bf13bcffb61", + "source": "No app is available to paste into.", + "translated": "붙여넣을 수 있는 앱이 없습니다." + }, + { + "id": "native.apple.bd2b8e99eee923b4", + "source": "Choose another app before pasting.", + "translated": "붙여넣기 전에 다른 앱을 선택하세요." + }, + { + "id": "native.apple.f7aeeb401f9a4cd1", + "source": "Accessibility permission is required to paste.", + "translated": "붙여넣으려면 손쉬운 사용 권한이 필요합니다." + }, + { + "id": "native.apple.14e9c9f41a9523c0", + "source": "Couldn't paste the reply.", + "translated": "답변을 붙여넣을 수 없습니다." + }, { "id": "native.apple.0a4fcdf09c5e7b3f", "source": "Capture Window…", @@ -21213,6 +21248,51 @@ "source": "Context from \\(context.appName) — \\(context.windowTitle)", "translated": "\\(context.appName)의 컨텍스트 — \\(context.windowTitle)" }, + { + "id": "native.apple.d82159d749697338", + "source": "Auto", + "translated": "자동" + }, + { + "id": "native.apple.4e02ff977e48dfcb", + "source": "\\(model) · \\(thinking)", + "translated": "\\(model) · \\(thinking)" + }, + { + "id": "native.apple.07bd035ae7e799c5", + "source": "Selected reasoning is unavailable for this target.", + "translated": "선택한 추론은 이 대상에서 사용할 수 없습니다." + }, + { + "id": "native.apple.2eb721ed8bffe9aa", + "source": "Couldn't load model settings.", + "translated": "모델 설정을 불러올 수 없습니다." + }, + { + "id": "native.apple.ef6f4c54f9cfac08", + "source": "Couldn't change the model.", + "translated": "모델을 변경할 수 없습니다." + }, + { + "id": "native.apple.82b81bd0670b0cef", + "source": "Model", + "translated": "모델" + }, + { + "id": "native.apple.2eb4c724364c7583", + "source": "Session default", + "translated": "세션 기본값" + }, + { + "id": "native.apple.9f1c4db792611774", + "source": "Reasoning", + "translated": "추론" + }, + { + "id": "native.apple.25a01f61b4e33ece", + "source": "Auto", + "translated": "자동" + }, { "id": "native.apple.b5524e37a230e571", "source": "New message to \\(agentName)", @@ -21248,6 +21328,16 @@ "source": "Send message", "translated": "메시지 보내기" }, + { + "id": "native.apple.b520a305b56a6387", + "source": "Choose model and reasoning", + "translated": "모델 및 추론 선택" + }, + { + "id": "native.apple.1dd50e6126da7179", + "source": "Model and reasoning", + "translated": "모델 및 추론" + }, { "id": "native.apple.024725d220e79697", "source": "Needs additional permissions", @@ -21273,6 +21363,21 @@ "source": "Remove attached text context", "translated": "첨부된 텍스트 컨텍스트 제거" }, + { + "id": "native.apple.00b46fe92d9bb1d4", + "source": "Paste to \\(self.model.frontmostAppName)", + "translated": "\\(self.model.frontmostAppName)에 붙여넣기" + }, + { + "id": "native.apple.d2aa2e60ec2f62d8", + "source": "Stop dictation", + "translated": "받아쓰기 중지" + }, + { + "id": "native.apple.8c4a27a4361d0572", + "source": "Start dictation", + "translated": "받아쓰기 시작" + }, { "id": "native.apple.88d0123c86b06144", "source": "Allow OpenClaw to capture windows", diff --git a/apps/.i18n/native/nl.json b/apps/.i18n/native/nl.json index b4d2051841b..1cf4babfa23 100644 --- a/apps/.i18n/native/nl.json +++ b/apps/.i18n/native/nl.json @@ -21118,6 +21118,41 @@ "source": "Continue", "translated": "Doorgaan" }, + { + "id": "native.apple.237f02309f0b9aeb", + "source": "Microphone and speech recognition permission required.", + "translated": "Toestemming voor microfoon en spraakherkenning vereist." + }, + { + "id": "native.apple.6f2679049010dae7", + "source": "Dictation stopped because speech recognition failed.", + "translated": "Dicteren gestopt omdat spraakherkenning is mislukt." + }, + { + "id": "native.apple.fd0469907ed8a50c", + "source": "Couldn't start dictation.", + "translated": "Kan dicteren niet starten." + }, + { + "id": "native.apple.ec789bf13bcffb61", + "source": "No app is available to paste into.", + "translated": "Er is geen app beschikbaar om in te plakken." + }, + { + "id": "native.apple.bd2b8e99eee923b4", + "source": "Choose another app before pasting.", + "translated": "Kies een andere app voordat je plakt." + }, + { + "id": "native.apple.f7aeeb401f9a4cd1", + "source": "Accessibility permission is required to paste.", + "translated": "Toestemming voor toegankelijkheid is vereist om te plakken." + }, + { + "id": "native.apple.14e9c9f41a9523c0", + "source": "Couldn't paste the reply.", + "translated": "Kan het antwoord niet plakken." + }, { "id": "native.apple.0a4fcdf09c5e7b3f", "source": "Capture Window…", @@ -21213,6 +21248,51 @@ "source": "Context from \\(context.appName) — \\(context.windowTitle)", "translated": "Context van \\(context.appName) — \\(context.windowTitle)" }, + { + "id": "native.apple.d82159d749697338", + "source": "Auto", + "translated": "Automatisch" + }, + { + "id": "native.apple.4e02ff977e48dfcb", + "source": "\\(model) · \\(thinking)", + "translated": "\\(model) · \\(thinking)" + }, + { + "id": "native.apple.07bd035ae7e799c5", + "source": "Selected reasoning is unavailable for this target.", + "translated": "De geselecteerde redeneermethode is niet beschikbaar voor dit doel." + }, + { + "id": "native.apple.2eb721ed8bffe9aa", + "source": "Couldn't load model settings.", + "translated": "Kan de modelinstellingen niet laden." + }, + { + "id": "native.apple.ef6f4c54f9cfac08", + "source": "Couldn't change the model.", + "translated": "Kan het model niet wijzigen." + }, + { + "id": "native.apple.82b81bd0670b0cef", + "source": "Model", + "translated": "Model" + }, + { + "id": "native.apple.2eb4c724364c7583", + "source": "Session default", + "translated": "Standaardinstelling voor sessie" + }, + { + "id": "native.apple.9f1c4db792611774", + "source": "Reasoning", + "translated": "Redeneren" + }, + { + "id": "native.apple.25a01f61b4e33ece", + "source": "Auto", + "translated": "Automatisch" + }, { "id": "native.apple.b5524e37a230e571", "source": "New message to \\(agentName)", @@ -21248,6 +21328,16 @@ "source": "Send message", "translated": "Bericht verzenden" }, + { + "id": "native.apple.b520a305b56a6387", + "source": "Choose model and reasoning", + "translated": "Kies model en redeneermethode" + }, + { + "id": "native.apple.1dd50e6126da7179", + "source": "Model and reasoning", + "translated": "Model en redeneermethode" + }, { "id": "native.apple.024725d220e79697", "source": "Needs additional permissions", @@ -21273,6 +21363,21 @@ "source": "Remove attached text context", "translated": "Bijgevoegde tekstcontext verwijderen" }, + { + "id": "native.apple.00b46fe92d9bb1d4", + "source": "Paste to \\(self.model.frontmostAppName)", + "translated": "Plakken in \\(self.model.frontmostAppName)" + }, + { + "id": "native.apple.d2aa2e60ec2f62d8", + "source": "Stop dictation", + "translated": "Stoppen met dicteren" + }, + { + "id": "native.apple.8c4a27a4361d0572", + "source": "Start dictation", + "translated": "Start dicteren" + }, { "id": "native.apple.88d0123c86b06144", "source": "Allow OpenClaw to capture windows", diff --git a/apps/.i18n/native/pl.json b/apps/.i18n/native/pl.json index 1aea60d8a2c..3ad3020ae64 100644 --- a/apps/.i18n/native/pl.json +++ b/apps/.i18n/native/pl.json @@ -21118,6 +21118,41 @@ "source": "Continue", "translated": "Kontynuuj" }, + { + "id": "native.apple.237f02309f0b9aeb", + "source": "Microphone and speech recognition permission required.", + "translated": "Wymagane są uprawnienia do mikrofonu i rozpoznawania mowy." + }, + { + "id": "native.apple.6f2679049010dae7", + "source": "Dictation stopped because speech recognition failed.", + "translated": "Dyktowanie zostało zatrzymane, ponieważ rozpoznawanie mowy nie powiodło się." + }, + { + "id": "native.apple.fd0469907ed8a50c", + "source": "Couldn't start dictation.", + "translated": "Nie udało się rozpocząć dyktowania." + }, + { + "id": "native.apple.ec789bf13bcffb61", + "source": "No app is available to paste into.", + "translated": "Brak aplikacji, do której można wkleić." + }, + { + "id": "native.apple.bd2b8e99eee923b4", + "source": "Choose another app before pasting.", + "translated": "Przed wklejeniem wybierz inną aplikację." + }, + { + "id": "native.apple.f7aeeb401f9a4cd1", + "source": "Accessibility permission is required to paste.", + "translated": "Do wklejania wymagane jest uprawnienie Dostępność." + }, + { + "id": "native.apple.14e9c9f41a9523c0", + "source": "Couldn't paste the reply.", + "translated": "Nie udało się wkleić odpowiedzi." + }, { "id": "native.apple.0a4fcdf09c5e7b3f", "source": "Capture Window…", @@ -21213,6 +21248,51 @@ "source": "Context from \\(context.appName) — \\(context.windowTitle)", "translated": "Kontekst z aplikacji \\(context.appName) — \\(context.windowTitle)" }, + { + "id": "native.apple.d82159d749697338", + "source": "Auto", + "translated": "Automatycznie" + }, + { + "id": "native.apple.4e02ff977e48dfcb", + "source": "\\(model) · \\(thinking)", + "translated": "\\(model) · \\(thinking)" + }, + { + "id": "native.apple.07bd035ae7e799c5", + "source": "Selected reasoning is unavailable for this target.", + "translated": "Wybrany tryb rozumowania jest niedostępny dla tego celu." + }, + { + "id": "native.apple.2eb721ed8bffe9aa", + "source": "Couldn't load model settings.", + "translated": "Nie udało się wczytać ustawień modelu." + }, + { + "id": "native.apple.ef6f4c54f9cfac08", + "source": "Couldn't change the model.", + "translated": "Nie udało się zmienić modelu." + }, + { + "id": "native.apple.82b81bd0670b0cef", + "source": "Model", + "translated": "Model" + }, + { + "id": "native.apple.2eb4c724364c7583", + "source": "Session default", + "translated": "Domyślne dla sesji" + }, + { + "id": "native.apple.9f1c4db792611774", + "source": "Reasoning", + "translated": "Rozumowanie" + }, + { + "id": "native.apple.25a01f61b4e33ece", + "source": "Auto", + "translated": "Automatycznie" + }, { "id": "native.apple.b5524e37a230e571", "source": "New message to \\(agentName)", @@ -21248,6 +21328,16 @@ "source": "Send message", "translated": "Wyślij wiadomość" }, + { + "id": "native.apple.b520a305b56a6387", + "source": "Choose model and reasoning", + "translated": "Wybierz model i tryb rozumowania" + }, + { + "id": "native.apple.1dd50e6126da7179", + "source": "Model and reasoning", + "translated": "Model i tryb rozumowania" + }, { "id": "native.apple.024725d220e79697", "source": "Needs additional permissions", @@ -21273,6 +21363,21 @@ "source": "Remove attached text context", "translated": "Usuń dołączony kontekst tekstowy" }, + { + "id": "native.apple.00b46fe92d9bb1d4", + "source": "Paste to \\(self.model.frontmostAppName)", + "translated": "Wklej do \\(self.model.frontmostAppName)" + }, + { + "id": "native.apple.d2aa2e60ec2f62d8", + "source": "Stop dictation", + "translated": "Zatrzymaj dyktowanie" + }, + { + "id": "native.apple.8c4a27a4361d0572", + "source": "Start dictation", + "translated": "Rozpocznij dyktowanie" + }, { "id": "native.apple.88d0123c86b06144", "source": "Allow OpenClaw to capture windows", diff --git a/apps/.i18n/native/pt-BR.json b/apps/.i18n/native/pt-BR.json index 6df52436710..8edc29496eb 100644 --- a/apps/.i18n/native/pt-BR.json +++ b/apps/.i18n/native/pt-BR.json @@ -21118,6 +21118,41 @@ "source": "Continue", "translated": "Continuar" }, + { + "id": "native.apple.237f02309f0b9aeb", + "source": "Microphone and speech recognition permission required.", + "translated": "É necessário permitir o acesso ao microfone e ao reconhecimento de fala." + }, + { + "id": "native.apple.6f2679049010dae7", + "source": "Dictation stopped because speech recognition failed.", + "translated": "O ditado foi interrompido porque o reconhecimento de fala falhou." + }, + { + "id": "native.apple.fd0469907ed8a50c", + "source": "Couldn't start dictation.", + "translated": "Não foi possível iniciar o ditado." + }, + { + "id": "native.apple.ec789bf13bcffb61", + "source": "No app is available to paste into.", + "translated": "Nenhum app está disponível para colar." + }, + { + "id": "native.apple.bd2b8e99eee923b4", + "source": "Choose another app before pasting.", + "translated": "Escolha outro app antes de colar." + }, + { + "id": "native.apple.f7aeeb401f9a4cd1", + "source": "Accessibility permission is required to paste.", + "translated": "É necessário conceder permissão de acessibilidade para colar." + }, + { + "id": "native.apple.14e9c9f41a9523c0", + "source": "Couldn't paste the reply.", + "translated": "Não foi possível colar a resposta." + }, { "id": "native.apple.0a4fcdf09c5e7b3f", "source": "Capture Window…", @@ -21213,6 +21248,51 @@ "source": "Context from \\(context.appName) — \\(context.windowTitle)", "translated": "Contexto de \\(context.appName) — \\(context.windowTitle)" }, + { + "id": "native.apple.d82159d749697338", + "source": "Auto", + "translated": "Automático" + }, + { + "id": "native.apple.4e02ff977e48dfcb", + "source": "\\(model) · \\(thinking)", + "translated": "\\(model) · \\(thinking)" + }, + { + "id": "native.apple.07bd035ae7e799c5", + "source": "Selected reasoning is unavailable for this target.", + "translated": "O raciocínio selecionado não está disponível para este destino." + }, + { + "id": "native.apple.2eb721ed8bffe9aa", + "source": "Couldn't load model settings.", + "translated": "Não foi possível carregar as configurações do modelo." + }, + { + "id": "native.apple.ef6f4c54f9cfac08", + "source": "Couldn't change the model.", + "translated": "Não foi possível alterar o modelo." + }, + { + "id": "native.apple.82b81bd0670b0cef", + "source": "Model", + "translated": "Modelo" + }, + { + "id": "native.apple.2eb4c724364c7583", + "source": "Session default", + "translated": "Padrão da sessão" + }, + { + "id": "native.apple.9f1c4db792611774", + "source": "Reasoning", + "translated": "Raciocínio" + }, + { + "id": "native.apple.25a01f61b4e33ece", + "source": "Auto", + "translated": "Automático" + }, { "id": "native.apple.b5524e37a230e571", "source": "New message to \\(agentName)", @@ -21248,6 +21328,16 @@ "source": "Send message", "translated": "Enviar mensagem" }, + { + "id": "native.apple.b520a305b56a6387", + "source": "Choose model and reasoning", + "translated": "Escolher modelo e raciocínio" + }, + { + "id": "native.apple.1dd50e6126da7179", + "source": "Model and reasoning", + "translated": "Modelo e raciocínio" + }, { "id": "native.apple.024725d220e79697", "source": "Needs additional permissions", @@ -21273,6 +21363,21 @@ "source": "Remove attached text context", "translated": "Remover contexto de texto anexado" }, + { + "id": "native.apple.00b46fe92d9bb1d4", + "source": "Paste to \\(self.model.frontmostAppName)", + "translated": "Colar em \\(self.model.frontmostAppName)" + }, + { + "id": "native.apple.d2aa2e60ec2f62d8", + "source": "Stop dictation", + "translated": "Parar ditado" + }, + { + "id": "native.apple.8c4a27a4361d0572", + "source": "Start dictation", + "translated": "Iniciar ditado" + }, { "id": "native.apple.88d0123c86b06144", "source": "Allow OpenClaw to capture windows", diff --git a/apps/.i18n/native/ru.json b/apps/.i18n/native/ru.json index 3fd73d703ea..15582e2b366 100644 --- a/apps/.i18n/native/ru.json +++ b/apps/.i18n/native/ru.json @@ -21118,6 +21118,41 @@ "source": "Continue", "translated": "Продолжить" }, + { + "id": "native.apple.237f02309f0b9aeb", + "source": "Microphone and speech recognition permission required.", + "translated": "Требуется разрешение на доступ к микрофону и распознаванию речи." + }, + { + "id": "native.apple.6f2679049010dae7", + "source": "Dictation stopped because speech recognition failed.", + "translated": "Диктовка остановлена из-за ошибки распознавания речи." + }, + { + "id": "native.apple.fd0469907ed8a50c", + "source": "Couldn't start dictation.", + "translated": "Не удалось начать диктовку." + }, + { + "id": "native.apple.ec789bf13bcffb61", + "source": "No app is available to paste into.", + "translated": "Нет доступного приложения для вставки." + }, + { + "id": "native.apple.bd2b8e99eee923b4", + "source": "Choose another app before pasting.", + "translated": "Перед вставкой выберите другое приложение." + }, + { + "id": "native.apple.f7aeeb401f9a4cd1", + "source": "Accessibility permission is required to paste.", + "translated": "Для вставки требуется разрешение на универсальный доступ." + }, + { + "id": "native.apple.14e9c9f41a9523c0", + "source": "Couldn't paste the reply.", + "translated": "Не удалось вставить ответ." + }, { "id": "native.apple.0a4fcdf09c5e7b3f", "source": "Capture Window…", @@ -21213,6 +21248,51 @@ "source": "Context from \\(context.appName) — \\(context.windowTitle)", "translated": "Контекст из \\(context.appName) — \\(context.windowTitle)" }, + { + "id": "native.apple.d82159d749697338", + "source": "Auto", + "translated": "Авто" + }, + { + "id": "native.apple.4e02ff977e48dfcb", + "source": "\\(model) · \\(thinking)", + "translated": "\\(model) · \\(thinking)" + }, + { + "id": "native.apple.07bd035ae7e799c5", + "source": "Selected reasoning is unavailable for this target.", + "translated": "Выбранный режим рассуждений недоступен для этой цели." + }, + { + "id": "native.apple.2eb721ed8bffe9aa", + "source": "Couldn't load model settings.", + "translated": "Не удалось загрузить настройки модели." + }, + { + "id": "native.apple.ef6f4c54f9cfac08", + "source": "Couldn't change the model.", + "translated": "Не удалось изменить модель." + }, + { + "id": "native.apple.82b81bd0670b0cef", + "source": "Model", + "translated": "Модель" + }, + { + "id": "native.apple.2eb4c724364c7583", + "source": "Session default", + "translated": "По умолчанию для сеанса" + }, + { + "id": "native.apple.9f1c4db792611774", + "source": "Reasoning", + "translated": "Рассуждения" + }, + { + "id": "native.apple.25a01f61b4e33ece", + "source": "Auto", + "translated": "Авто" + }, { "id": "native.apple.b5524e37a230e571", "source": "New message to \\(agentName)", @@ -21248,6 +21328,16 @@ "source": "Send message", "translated": "Отправить сообщение" }, + { + "id": "native.apple.b520a305b56a6387", + "source": "Choose model and reasoning", + "translated": "Выбрать модель и режим рассуждений" + }, + { + "id": "native.apple.1dd50e6126da7179", + "source": "Model and reasoning", + "translated": "Модель и режим рассуждений" + }, { "id": "native.apple.024725d220e79697", "source": "Needs additional permissions", @@ -21273,6 +21363,21 @@ "source": "Remove attached text context", "translated": "Удалить прикреплённый текстовый контекст" }, + { + "id": "native.apple.00b46fe92d9bb1d4", + "source": "Paste to \\(self.model.frontmostAppName)", + "translated": "Вставить в \\(self.model.frontmostAppName)" + }, + { + "id": "native.apple.d2aa2e60ec2f62d8", + "source": "Stop dictation", + "translated": "Остановить диктовку" + }, + { + "id": "native.apple.8c4a27a4361d0572", + "source": "Start dictation", + "translated": "Начать диктовку" + }, { "id": "native.apple.88d0123c86b06144", "source": "Allow OpenClaw to capture windows", diff --git a/apps/.i18n/native/sv.json b/apps/.i18n/native/sv.json index bcdf8bf435f..6e08d6c71e7 100644 --- a/apps/.i18n/native/sv.json +++ b/apps/.i18n/native/sv.json @@ -21118,6 +21118,41 @@ "source": "Continue", "translated": "Fortsätt" }, + { + "id": "native.apple.237f02309f0b9aeb", + "source": "Microphone and speech recognition permission required.", + "translated": "Behörighet för mikrofon och taligenkänning krävs." + }, + { + "id": "native.apple.6f2679049010dae7", + "source": "Dictation stopped because speech recognition failed.", + "translated": "Dikteringen stoppades eftersom taligenkänningen misslyckades." + }, + { + "id": "native.apple.fd0469907ed8a50c", + "source": "Couldn't start dictation.", + "translated": "Det gick inte att starta dikteringen." + }, + { + "id": "native.apple.ec789bf13bcffb61", + "source": "No app is available to paste into.", + "translated": "Det finns ingen app att klistra in i." + }, + { + "id": "native.apple.bd2b8e99eee923b4", + "source": "Choose another app before pasting.", + "translated": "Välj en annan app innan du klistrar in." + }, + { + "id": "native.apple.f7aeeb401f9a4cd1", + "source": "Accessibility permission is required to paste.", + "translated": "Behörighet för hjälpmedel krävs för att klistra in." + }, + { + "id": "native.apple.14e9c9f41a9523c0", + "source": "Couldn't paste the reply.", + "translated": "Det gick inte att klistra in svaret." + }, { "id": "native.apple.0a4fcdf09c5e7b3f", "source": "Capture Window…", @@ -21213,6 +21248,51 @@ "source": "Context from \\(context.appName) — \\(context.windowTitle)", "translated": "Kontext från \\(context.appName) — \\(context.windowTitle)" }, + { + "id": "native.apple.d82159d749697338", + "source": "Auto", + "translated": "Automatiskt" + }, + { + "id": "native.apple.4e02ff977e48dfcb", + "source": "\\(model) · \\(thinking)", + "translated": "\\(model) · \\(thinking)" + }, + { + "id": "native.apple.07bd035ae7e799c5", + "source": "Selected reasoning is unavailable for this target.", + "translated": "Det valda resonemanget är inte tillgängligt för det här målet." + }, + { + "id": "native.apple.2eb721ed8bffe9aa", + "source": "Couldn't load model settings.", + "translated": "Det gick inte att läsa in modellinställningarna." + }, + { + "id": "native.apple.ef6f4c54f9cfac08", + "source": "Couldn't change the model.", + "translated": "Det gick inte att byta modell." + }, + { + "id": "native.apple.82b81bd0670b0cef", + "source": "Model", + "translated": "Modell" + }, + { + "id": "native.apple.2eb4c724364c7583", + "source": "Session default", + "translated": "Sessionsstandard" + }, + { + "id": "native.apple.9f1c4db792611774", + "source": "Reasoning", + "translated": "Resonemang" + }, + { + "id": "native.apple.25a01f61b4e33ece", + "source": "Auto", + "translated": "Automatiskt" + }, { "id": "native.apple.b5524e37a230e571", "source": "New message to \\(agentName)", @@ -21248,6 +21328,16 @@ "source": "Send message", "translated": "Skicka meddelande" }, + { + "id": "native.apple.b520a305b56a6387", + "source": "Choose model and reasoning", + "translated": "Välj modell och resonemang" + }, + { + "id": "native.apple.1dd50e6126da7179", + "source": "Model and reasoning", + "translated": "Modell och resonemang" + }, { "id": "native.apple.024725d220e79697", "source": "Needs additional permissions", @@ -21273,6 +21363,21 @@ "source": "Remove attached text context", "translated": "Ta bort bifogad textkontext" }, + { + "id": "native.apple.00b46fe92d9bb1d4", + "source": "Paste to \\(self.model.frontmostAppName)", + "translated": "Klistra in i \\(self.model.frontmostAppName)" + }, + { + "id": "native.apple.d2aa2e60ec2f62d8", + "source": "Stop dictation", + "translated": "Stoppa diktering" + }, + { + "id": "native.apple.8c4a27a4361d0572", + "source": "Start dictation", + "translated": "Starta diktering" + }, { "id": "native.apple.88d0123c86b06144", "source": "Allow OpenClaw to capture windows", diff --git a/apps/.i18n/native/th.json b/apps/.i18n/native/th.json index bea87b244fa..1e533575de5 100644 --- a/apps/.i18n/native/th.json +++ b/apps/.i18n/native/th.json @@ -21118,6 +21118,41 @@ "source": "Continue", "translated": "ดำเนินการต่อ" }, + { + "id": "native.apple.237f02309f0b9aeb", + "source": "Microphone and speech recognition permission required.", + "translated": "ต้องอนุญาตการเข้าถึงไมโครโฟนและการรู้จำเสียงพูด" + }, + { + "id": "native.apple.6f2679049010dae7", + "source": "Dictation stopped because speech recognition failed.", + "translated": "การป้อนตามคำบอกหยุดลงเนื่องจากการรู้จำเสียงพูดล้มเหลว" + }, + { + "id": "native.apple.fd0469907ed8a50c", + "source": "Couldn't start dictation.", + "translated": "ไม่สามารถเริ่มการป้อนตามคำบอกได้" + }, + { + "id": "native.apple.ec789bf13bcffb61", + "source": "No app is available to paste into.", + "translated": "ไม่มีแอปที่สามารถวางข้อความได้" + }, + { + "id": "native.apple.bd2b8e99eee923b4", + "source": "Choose another app before pasting.", + "translated": "เลือกแอปอื่นก่อนวาง" + }, + { + "id": "native.apple.f7aeeb401f9a4cd1", + "source": "Accessibility permission is required to paste.", + "translated": "ต้องอนุญาตการช่วยการเข้าถึงเพื่อวางข้อความ" + }, + { + "id": "native.apple.14e9c9f41a9523c0", + "source": "Couldn't paste the reply.", + "translated": "ไม่สามารถวางคำตอบได้" + }, { "id": "native.apple.0a4fcdf09c5e7b3f", "source": "Capture Window…", @@ -21213,6 +21248,51 @@ "source": "Context from \\(context.appName) — \\(context.windowTitle)", "translated": "บริบทจาก \\(context.appName) — \\(context.windowTitle)" }, + { + "id": "native.apple.d82159d749697338", + "source": "Auto", + "translated": "อัตโนมัติ" + }, + { + "id": "native.apple.4e02ff977e48dfcb", + "source": "\\(model) · \\(thinking)", + "translated": "\\(model) · \\(thinking)" + }, + { + "id": "native.apple.07bd035ae7e799c5", + "source": "Selected reasoning is unavailable for this target.", + "translated": "การใช้เหตุผลที่เลือกไม่พร้อมใช้งานสำหรับเป้าหมายนี้" + }, + { + "id": "native.apple.2eb721ed8bffe9aa", + "source": "Couldn't load model settings.", + "translated": "ไม่สามารถโหลดการตั้งค่าโมเดลได้" + }, + { + "id": "native.apple.ef6f4c54f9cfac08", + "source": "Couldn't change the model.", + "translated": "ไม่สามารถเปลี่ยนโมเดลได้" + }, + { + "id": "native.apple.82b81bd0670b0cef", + "source": "Model", + "translated": "โมเดล" + }, + { + "id": "native.apple.2eb4c724364c7583", + "source": "Session default", + "translated": "ค่าเริ่มต้นของเซสชัน" + }, + { + "id": "native.apple.9f1c4db792611774", + "source": "Reasoning", + "translated": "การใช้เหตุผล" + }, + { + "id": "native.apple.25a01f61b4e33ece", + "source": "Auto", + "translated": "อัตโนมัติ" + }, { "id": "native.apple.b5524e37a230e571", "source": "New message to \\(agentName)", @@ -21248,6 +21328,16 @@ "source": "Send message", "translated": "ส่งข้อความ" }, + { + "id": "native.apple.b520a305b56a6387", + "source": "Choose model and reasoning", + "translated": "เลือกโมเดลและการใช้เหตุผล" + }, + { + "id": "native.apple.1dd50e6126da7179", + "source": "Model and reasoning", + "translated": "โมเดลและการใช้เหตุผล" + }, { "id": "native.apple.024725d220e79697", "source": "Needs additional permissions", @@ -21273,6 +21363,21 @@ "source": "Remove attached text context", "translated": "ลบบริบทข้อความที่แนบมา" }, + { + "id": "native.apple.00b46fe92d9bb1d4", + "source": "Paste to \\(self.model.frontmostAppName)", + "translated": "วางไปยัง \\(self.model.frontmostAppName)" + }, + { + "id": "native.apple.d2aa2e60ec2f62d8", + "source": "Stop dictation", + "translated": "หยุดการป้อนตามคำบอก" + }, + { + "id": "native.apple.8c4a27a4361d0572", + "source": "Start dictation", + "translated": "เริ่มการป้อนตามคำบอก" + }, { "id": "native.apple.88d0123c86b06144", "source": "Allow OpenClaw to capture windows", diff --git a/apps/.i18n/native/tr.json b/apps/.i18n/native/tr.json index 723d0a510dd..3136f590b9a 100644 --- a/apps/.i18n/native/tr.json +++ b/apps/.i18n/native/tr.json @@ -21118,6 +21118,41 @@ "source": "Continue", "translated": "Devam et" }, + { + "id": "native.apple.237f02309f0b9aeb", + "source": "Microphone and speech recognition permission required.", + "translated": "Mikrofon ve konuşma tanıma izni gerekiyor." + }, + { + "id": "native.apple.6f2679049010dae7", + "source": "Dictation stopped because speech recognition failed.", + "translated": "Konuşma tanıma başarısız olduğu için dikte durduruldu." + }, + { + "id": "native.apple.fd0469907ed8a50c", + "source": "Couldn't start dictation.", + "translated": "Dikte başlatılamadı." + }, + { + "id": "native.apple.ec789bf13bcffb61", + "source": "No app is available to paste into.", + "translated": "Yapıştırılabilecek bir uygulama yok." + }, + { + "id": "native.apple.bd2b8e99eee923b4", + "source": "Choose another app before pasting.", + "translated": "Yapıştırmadan önce başka bir uygulama seçin." + }, + { + "id": "native.apple.f7aeeb401f9a4cd1", + "source": "Accessibility permission is required to paste.", + "translated": "Yapıştırmak için Erişilebilirlik izni gerekiyor." + }, + { + "id": "native.apple.14e9c9f41a9523c0", + "source": "Couldn't paste the reply.", + "translated": "Yanıt yapıştırılamadı." + }, { "id": "native.apple.0a4fcdf09c5e7b3f", "source": "Capture Window…", @@ -21213,6 +21248,51 @@ "source": "Context from \\(context.appName) — \\(context.windowTitle)", "translated": "\\(context.appName) bağlamı — \\(context.windowTitle)" }, + { + "id": "native.apple.d82159d749697338", + "source": "Auto", + "translated": "Otomatik" + }, + { + "id": "native.apple.4e02ff977e48dfcb", + "source": "\\(model) · \\(thinking)", + "translated": "\\(model) · \\(thinking)" + }, + { + "id": "native.apple.07bd035ae7e799c5", + "source": "Selected reasoning is unavailable for this target.", + "translated": "Seçilen akıl yürütme bu hedef için kullanılamıyor." + }, + { + "id": "native.apple.2eb721ed8bffe9aa", + "source": "Couldn't load model settings.", + "translated": "Model ayarları yüklenemedi." + }, + { + "id": "native.apple.ef6f4c54f9cfac08", + "source": "Couldn't change the model.", + "translated": "Model değiştirilemedi." + }, + { + "id": "native.apple.82b81bd0670b0cef", + "source": "Model", + "translated": "Model" + }, + { + "id": "native.apple.2eb4c724364c7583", + "source": "Session default", + "translated": "Oturum varsayılanı" + }, + { + "id": "native.apple.9f1c4db792611774", + "source": "Reasoning", + "translated": "Akıl yürütme" + }, + { + "id": "native.apple.25a01f61b4e33ece", + "source": "Auto", + "translated": "Otomatik" + }, { "id": "native.apple.b5524e37a230e571", "source": "New message to \\(agentName)", @@ -21248,6 +21328,16 @@ "source": "Send message", "translated": "Mesaj gönder" }, + { + "id": "native.apple.b520a305b56a6387", + "source": "Choose model and reasoning", + "translated": "Model ve akıl yürütme seçin" + }, + { + "id": "native.apple.1dd50e6126da7179", + "source": "Model and reasoning", + "translated": "Model ve akıl yürütme" + }, { "id": "native.apple.024725d220e79697", "source": "Needs additional permissions", @@ -21273,6 +21363,21 @@ "source": "Remove attached text context", "translated": "Ekli metin bağlamını kaldır" }, + { + "id": "native.apple.00b46fe92d9bb1d4", + "source": "Paste to \\(self.model.frontmostAppName)", + "translated": "\\(self.model.frontmostAppName) uygulamasına yapıştır" + }, + { + "id": "native.apple.d2aa2e60ec2f62d8", + "source": "Stop dictation", + "translated": "Dikteyi durdur" + }, + { + "id": "native.apple.8c4a27a4361d0572", + "source": "Start dictation", + "translated": "Dikteyi başlat" + }, { "id": "native.apple.88d0123c86b06144", "source": "Allow OpenClaw to capture windows", diff --git a/apps/.i18n/native/uk.json b/apps/.i18n/native/uk.json index 9ebb15ecb19..7017c159711 100644 --- a/apps/.i18n/native/uk.json +++ b/apps/.i18n/native/uk.json @@ -21118,6 +21118,41 @@ "source": "Continue", "translated": "Продовжити" }, + { + "id": "native.apple.237f02309f0b9aeb", + "source": "Microphone and speech recognition permission required.", + "translated": "Потрібен дозвіл на використання мікрофона та розпізнавання мовлення." + }, + { + "id": "native.apple.6f2679049010dae7", + "source": "Dictation stopped because speech recognition failed.", + "translated": "Диктування зупинено через помилку розпізнавання мовлення." + }, + { + "id": "native.apple.fd0469907ed8a50c", + "source": "Couldn't start dictation.", + "translated": "Не вдалося почати диктування." + }, + { + "id": "native.apple.ec789bf13bcffb61", + "source": "No app is available to paste into.", + "translated": "Немає доступної програми для вставлення." + }, + { + "id": "native.apple.bd2b8e99eee923b4", + "source": "Choose another app before pasting.", + "translated": "Перед вставленням виберіть іншу програму." + }, + { + "id": "native.apple.f7aeeb401f9a4cd1", + "source": "Accessibility permission is required to paste.", + "translated": "Для вставлення потрібен дозвіл на універсальний доступ." + }, + { + "id": "native.apple.14e9c9f41a9523c0", + "source": "Couldn't paste the reply.", + "translated": "Не вдалося вставити відповідь." + }, { "id": "native.apple.0a4fcdf09c5e7b3f", "source": "Capture Window…", @@ -21213,6 +21248,51 @@ "source": "Context from \\(context.appName) — \\(context.windowTitle)", "translated": "Контекст із \\(context.appName) — \\(context.windowTitle)" }, + { + "id": "native.apple.d82159d749697338", + "source": "Auto", + "translated": "Автоматично" + }, + { + "id": "native.apple.4e02ff977e48dfcb", + "source": "\\(model) · \\(thinking)", + "translated": "\\(model) · \\(thinking)" + }, + { + "id": "native.apple.07bd035ae7e799c5", + "source": "Selected reasoning is unavailable for this target.", + "translated": "Вибраний режим міркування недоступний для цієї цілі." + }, + { + "id": "native.apple.2eb721ed8bffe9aa", + "source": "Couldn't load model settings.", + "translated": "Не вдалося завантажити налаштування моделі." + }, + { + "id": "native.apple.ef6f4c54f9cfac08", + "source": "Couldn't change the model.", + "translated": "Не вдалося змінити модель." + }, + { + "id": "native.apple.82b81bd0670b0cef", + "source": "Model", + "translated": "Модель" + }, + { + "id": "native.apple.2eb4c724364c7583", + "source": "Session default", + "translated": "Типове значення сеансу" + }, + { + "id": "native.apple.9f1c4db792611774", + "source": "Reasoning", + "translated": "Міркування" + }, + { + "id": "native.apple.25a01f61b4e33ece", + "source": "Auto", + "translated": "Автоматично" + }, { "id": "native.apple.b5524e37a230e571", "source": "New message to \\(agentName)", @@ -21248,6 +21328,16 @@ "source": "Send message", "translated": "Надіслати повідомлення" }, + { + "id": "native.apple.b520a305b56a6387", + "source": "Choose model and reasoning", + "translated": "Виберіть модель і режим міркування" + }, + { + "id": "native.apple.1dd50e6126da7179", + "source": "Model and reasoning", + "translated": "Модель і режим міркування" + }, { "id": "native.apple.024725d220e79697", "source": "Needs additional permissions", @@ -21273,6 +21363,21 @@ "source": "Remove attached text context", "translated": "Видалити прикріплений текстовий контекст" }, + { + "id": "native.apple.00b46fe92d9bb1d4", + "source": "Paste to \\(self.model.frontmostAppName)", + "translated": "Вставити в \\(self.model.frontmostAppName)" + }, + { + "id": "native.apple.d2aa2e60ec2f62d8", + "source": "Stop dictation", + "translated": "Зупинити диктування" + }, + { + "id": "native.apple.8c4a27a4361d0572", + "source": "Start dictation", + "translated": "Почати диктування" + }, { "id": "native.apple.88d0123c86b06144", "source": "Allow OpenClaw to capture windows", diff --git a/apps/.i18n/native/vi.json b/apps/.i18n/native/vi.json index bec9320685e..2ea35dc647e 100644 --- a/apps/.i18n/native/vi.json +++ b/apps/.i18n/native/vi.json @@ -21118,6 +21118,41 @@ "source": "Continue", "translated": "Tiếp tục" }, + { + "id": "native.apple.237f02309f0b9aeb", + "source": "Microphone and speech recognition permission required.", + "translated": "Cần có quyền truy cập micrô và nhận dạng giọng nói." + }, + { + "id": "native.apple.6f2679049010dae7", + "source": "Dictation stopped because speech recognition failed.", + "translated": "Đã dừng đọc chính tả vì nhận dạng giọng nói không thành công." + }, + { + "id": "native.apple.fd0469907ed8a50c", + "source": "Couldn't start dictation.", + "translated": "Không thể bắt đầu đọc chính tả." + }, + { + "id": "native.apple.ec789bf13bcffb61", + "source": "No app is available to paste into.", + "translated": "Không có ứng dụng nào khả dụng để dán vào." + }, + { + "id": "native.apple.bd2b8e99eee923b4", + "source": "Choose another app before pasting.", + "translated": "Chọn một ứng dụng khác trước khi dán." + }, + { + "id": "native.apple.f7aeeb401f9a4cd1", + "source": "Accessibility permission is required to paste.", + "translated": "Cần có quyền Trợ năng để dán." + }, + { + "id": "native.apple.14e9c9f41a9523c0", + "source": "Couldn't paste the reply.", + "translated": "Không thể dán câu trả lời." + }, { "id": "native.apple.0a4fcdf09c5e7b3f", "source": "Capture Window…", @@ -21213,6 +21248,51 @@ "source": "Context from \\(context.appName) — \\(context.windowTitle)", "translated": "Ngữ cảnh từ \\(context.appName) — \\(context.windowTitle)" }, + { + "id": "native.apple.d82159d749697338", + "source": "Auto", + "translated": "Tự động" + }, + { + "id": "native.apple.4e02ff977e48dfcb", + "source": "\\(model) · \\(thinking)", + "translated": "\\(model) · \\(thinking)" + }, + { + "id": "native.apple.07bd035ae7e799c5", + "source": "Selected reasoning is unavailable for this target.", + "translated": "Chế độ suy luận đã chọn không khả dụng cho đích này." + }, + { + "id": "native.apple.2eb721ed8bffe9aa", + "source": "Couldn't load model settings.", + "translated": "Không thể tải cài đặt mô hình." + }, + { + "id": "native.apple.ef6f4c54f9cfac08", + "source": "Couldn't change the model.", + "translated": "Không thể thay đổi mô hình." + }, + { + "id": "native.apple.82b81bd0670b0cef", + "source": "Model", + "translated": "Mô hình" + }, + { + "id": "native.apple.2eb4c724364c7583", + "source": "Session default", + "translated": "Mặc định của phiên" + }, + { + "id": "native.apple.9f1c4db792611774", + "source": "Reasoning", + "translated": "Suy luận" + }, + { + "id": "native.apple.25a01f61b4e33ece", + "source": "Auto", + "translated": "Tự động" + }, { "id": "native.apple.b5524e37a230e571", "source": "New message to \\(agentName)", @@ -21248,6 +21328,16 @@ "source": "Send message", "translated": "Gửi tin nhắn" }, + { + "id": "native.apple.b520a305b56a6387", + "source": "Choose model and reasoning", + "translated": "Chọn mô hình và chế độ suy luận" + }, + { + "id": "native.apple.1dd50e6126da7179", + "source": "Model and reasoning", + "translated": "Mô hình và chế độ suy luận" + }, { "id": "native.apple.024725d220e79697", "source": "Needs additional permissions", @@ -21273,6 +21363,21 @@ "source": "Remove attached text context", "translated": "Xóa ngữ cảnh văn bản đính kèm" }, + { + "id": "native.apple.00b46fe92d9bb1d4", + "source": "Paste to \\(self.model.frontmostAppName)", + "translated": "Dán vào \\(self.model.frontmostAppName)" + }, + { + "id": "native.apple.d2aa2e60ec2f62d8", + "source": "Stop dictation", + "translated": "Dừng đọc chính tả" + }, + { + "id": "native.apple.8c4a27a4361d0572", + "source": "Start dictation", + "translated": "Bắt đầu đọc chính tả" + }, { "id": "native.apple.88d0123c86b06144", "source": "Allow OpenClaw to capture windows", diff --git a/apps/.i18n/native/zh-CN.json b/apps/.i18n/native/zh-CN.json index 5028eae0acd..35616662e73 100644 --- a/apps/.i18n/native/zh-CN.json +++ b/apps/.i18n/native/zh-CN.json @@ -21118,6 +21118,41 @@ "source": "Continue", "translated": "继续" }, + { + "id": "native.apple.237f02309f0b9aeb", + "source": "Microphone and speech recognition permission required.", + "translated": "需要麦克风和语音识别权限。" + }, + { + "id": "native.apple.6f2679049010dae7", + "source": "Dictation stopped because speech recognition failed.", + "translated": "由于语音识别失败,听写已停止。" + }, + { + "id": "native.apple.fd0469907ed8a50c", + "source": "Couldn't start dictation.", + "translated": "无法开始听写。" + }, + { + "id": "native.apple.ec789bf13bcffb61", + "source": "No app is available to paste into.", + "translated": "没有可粘贴到的应用。" + }, + { + "id": "native.apple.bd2b8e99eee923b4", + "source": "Choose another app before pasting.", + "translated": "请先选择其他应用再粘贴。" + }, + { + "id": "native.apple.f7aeeb401f9a4cd1", + "source": "Accessibility permission is required to paste.", + "translated": "需要辅助功能权限才能粘贴。" + }, + { + "id": "native.apple.14e9c9f41a9523c0", + "source": "Couldn't paste the reply.", + "translated": "无法粘贴回复。" + }, { "id": "native.apple.0a4fcdf09c5e7b3f", "source": "Capture Window…", @@ -21213,6 +21248,51 @@ "source": "Context from \\(context.appName) — \\(context.windowTitle)", "translated": "来自 \\(context.appName) 的上下文 — \\(context.windowTitle)" }, + { + "id": "native.apple.d82159d749697338", + "source": "Auto", + "translated": "自动" + }, + { + "id": "native.apple.4e02ff977e48dfcb", + "source": "\\(model) · \\(thinking)", + "translated": "\\(model) · \\(thinking)" + }, + { + "id": "native.apple.07bd035ae7e799c5", + "source": "Selected reasoning is unavailable for this target.", + "translated": "所选推理模式不适用于此目标。" + }, + { + "id": "native.apple.2eb721ed8bffe9aa", + "source": "Couldn't load model settings.", + "translated": "无法加载模型设置。" + }, + { + "id": "native.apple.ef6f4c54f9cfac08", + "source": "Couldn't change the model.", + "translated": "无法更改模型。" + }, + { + "id": "native.apple.82b81bd0670b0cef", + "source": "Model", + "translated": "模型" + }, + { + "id": "native.apple.2eb4c724364c7583", + "source": "Session default", + "translated": "会话默认设置" + }, + { + "id": "native.apple.9f1c4db792611774", + "source": "Reasoning", + "translated": "推理" + }, + { + "id": "native.apple.25a01f61b4e33ece", + "source": "Auto", + "translated": "自动" + }, { "id": "native.apple.b5524e37a230e571", "source": "New message to \\(agentName)", @@ -21248,6 +21328,16 @@ "source": "Send message", "translated": "发送消息" }, + { + "id": "native.apple.b520a305b56a6387", + "source": "Choose model and reasoning", + "translated": "选择模型和推理模式" + }, + { + "id": "native.apple.1dd50e6126da7179", + "source": "Model and reasoning", + "translated": "模型和推理模式" + }, { "id": "native.apple.024725d220e79697", "source": "Needs additional permissions", @@ -21273,6 +21363,21 @@ "source": "Remove attached text context", "translated": "移除附加的文本上下文" }, + { + "id": "native.apple.00b46fe92d9bb1d4", + "source": "Paste to \\(self.model.frontmostAppName)", + "translated": "粘贴到 \\(self.model.frontmostAppName)" + }, + { + "id": "native.apple.d2aa2e60ec2f62d8", + "source": "Stop dictation", + "translated": "停止听写" + }, + { + "id": "native.apple.8c4a27a4361d0572", + "source": "Start dictation", + "translated": "开始听写" + }, { "id": "native.apple.88d0123c86b06144", "source": "Allow OpenClaw to capture windows", diff --git a/apps/.i18n/native/zh-TW.json b/apps/.i18n/native/zh-TW.json index 08d4da60dca..e372327a536 100644 --- a/apps/.i18n/native/zh-TW.json +++ b/apps/.i18n/native/zh-TW.json @@ -21118,6 +21118,41 @@ "source": "Continue", "translated": "繼續" }, + { + "id": "native.apple.237f02309f0b9aeb", + "source": "Microphone and speech recognition permission required.", + "translated": "需要麥克風與語音辨識權限。" + }, + { + "id": "native.apple.6f2679049010dae7", + "source": "Dictation stopped because speech recognition failed.", + "translated": "因語音辨識失敗,聽寫已停止。" + }, + { + "id": "native.apple.fd0469907ed8a50c", + "source": "Couldn't start dictation.", + "translated": "無法開始聽寫。" + }, + { + "id": "native.apple.ec789bf13bcffb61", + "source": "No app is available to paste into.", + "translated": "沒有可貼上的 App。" + }, + { + "id": "native.apple.bd2b8e99eee923b4", + "source": "Choose another app before pasting.", + "translated": "請先選擇其他 App,再進行貼上。" + }, + { + "id": "native.apple.f7aeeb401f9a4cd1", + "source": "Accessibility permission is required to paste.", + "translated": "需要輔助使用權限才能貼上。" + }, + { + "id": "native.apple.14e9c9f41a9523c0", + "source": "Couldn't paste the reply.", + "translated": "無法貼上回覆。" + }, { "id": "native.apple.0a4fcdf09c5e7b3f", "source": "Capture Window…", @@ -21213,6 +21248,51 @@ "source": "Context from \\(context.appName) — \\(context.windowTitle)", "translated": "來自 \\(context.appName) — \\(context.windowTitle) 的內容" }, + { + "id": "native.apple.d82159d749697338", + "source": "Auto", + "translated": "自動" + }, + { + "id": "native.apple.4e02ff977e48dfcb", + "source": "\\(model) · \\(thinking)", + "translated": "\\(model) · \\(thinking)" + }, + { + "id": "native.apple.07bd035ae7e799c5", + "source": "Selected reasoning is unavailable for this target.", + "translated": "此目標無法使用所選的推理模式。" + }, + { + "id": "native.apple.2eb721ed8bffe9aa", + "source": "Couldn't load model settings.", + "translated": "無法載入模型設定。" + }, + { + "id": "native.apple.ef6f4c54f9cfac08", + "source": "Couldn't change the model.", + "translated": "無法變更模型。" + }, + { + "id": "native.apple.82b81bd0670b0cef", + "source": "Model", + "translated": "模型" + }, + { + "id": "native.apple.2eb4c724364c7583", + "source": "Session default", + "translated": "工作階段預設值" + }, + { + "id": "native.apple.9f1c4db792611774", + "source": "Reasoning", + "translated": "推理" + }, + { + "id": "native.apple.25a01f61b4e33ece", + "source": "Auto", + "translated": "自動" + }, { "id": "native.apple.b5524e37a230e571", "source": "New message to \\(agentName)", @@ -21248,6 +21328,16 @@ "source": "Send message", "translated": "傳送訊息" }, + { + "id": "native.apple.b520a305b56a6387", + "source": "Choose model and reasoning", + "translated": "選擇模型與推理模式" + }, + { + "id": "native.apple.1dd50e6126da7179", + "source": "Model and reasoning", + "translated": "模型與推理模式" + }, { "id": "native.apple.024725d220e79697", "source": "Needs additional permissions", @@ -21273,6 +21363,21 @@ "source": "Remove attached text context", "translated": "移除附加的文字內容" }, + { + "id": "native.apple.00b46fe92d9bb1d4", + "source": "Paste to \\(self.model.frontmostAppName)", + "translated": "貼到 \\(self.model.frontmostAppName)" + }, + { + "id": "native.apple.d2aa2e60ec2f62d8", + "source": "Stop dictation", + "translated": "停止聽寫" + }, + { + "id": "native.apple.8c4a27a4361d0572", + "source": "Start dictation", + "translated": "開始聽寫" + }, { "id": "native.apple.88d0123c86b06144", "source": "Allow OpenClaw to capture windows", diff --git a/apps/macos/Sources/OpenClaw/QuickChatController+Testing.swift b/apps/macos/Sources/OpenClaw/QuickChatController+Testing.swift index 72493793aaa..adfbfab78c9 100644 --- a/apps/macos/Sources/OpenClaw/QuickChatController+Testing.swift +++ b/apps/macos/Sources/OpenClaw/QuickChatController+Testing.swift @@ -23,7 +23,7 @@ extension QuickChatController { agents: []) }, agentIdentityProvider: { _ in .placeholder }, - sendProvider: { _, _, _, _, _ in "started" }, + sendProvider: { _, _, _, _, _, _ in "started" }, permissionStatusProvider: { capabilities in Dictionary(uniqueKeysWithValues: capabilities.map { ($0, true) }) }, diff --git a/apps/macos/Sources/OpenClaw/QuickChatController.swift b/apps/macos/Sources/OpenClaw/QuickChatController.swift index 9f5ce1fa79b..95ed6c07e6e 100644 --- a/apps/macos/Sources/OpenClaw/QuickChatController.swift +++ b/apps/macos/Sources/OpenClaw/QuickChatController.swift @@ -97,6 +97,7 @@ final class QuickChatController: NSObject, NSWindowDelegate { @ObservationIgnored private let hotkeyRemover: HotkeyRemover @ObservationIgnored private let chatOpener: ChatOpener @ObservationIgnored private let recentSessionsProvider: RecentSessionsProvider + @ObservationIgnored private let dictation: QuickChatDictation @ObservationIgnored private let allowsHotkeyRegistrationInTests: Bool @ObservationIgnored private var panel: QuickChatPanel? @ObservationIgnored private var hostingView: NSHostingView? @@ -113,10 +114,15 @@ final class QuickChatController: NSObject, NSWindowDelegate { @ObservationIgnored private var isMenuActive = false @ObservationIgnored private var recentSessionsTask: Task? @ObservationIgnored private var recentSessionsRequestID = UUID() + @ObservationIgnored private var dictationStartTask: Task? + @ObservationIgnored private var dictationRequestID = UUID() + @ObservationIgnored private var pasteTask: Task? + @ObservationIgnored private var pasteRequestID = UUID() init( enableUI: Bool = true, model: QuickChatModel? = nil, + dictation: QuickChatDictation = QuickChatDictation(), monitoringEnabled: Bool? = nil, globalMonitorInstaller: @escaping GlobalMonitorInstaller = { mask, handler in NSEvent.addGlobalMonitorForEvents(matching: mask, handler: handler) @@ -150,6 +156,7 @@ final class QuickChatController: NSObject, NSWindowDelegate { { self.enableUI = enableUI self.model = model ?? QuickChatModel() + self.dictation = dictation self.replyBinding = QuickChatReplyBinding(viewModelFactory: replyViewModelFactory) self.monitoringEnabled = monitoringEnabled ?? (enableUI && !ProcessInfo.processInfo.isRunningTests) self.globalMonitorInstaller = globalMonitorInstaller @@ -163,6 +170,8 @@ final class QuickChatController: NSObject, NSWindowDelegate { super.init() self.model.onSendDispatched = { [weak self] route in guard let self else { return } + self.stopDictation() + self.cancelPasteRequest() // A dispatched send supersedes any pending recents fetch: its menu popping // over the fresh reply would rebind away from the response just sent. self.invalidateRecentsFetch() @@ -238,6 +247,8 @@ final class QuickChatController: NSObject, NSWindowDelegate { func present() { guard self.isEnabled else { return } self.transitionID = UUID() + self.stopDictation() + self.cancelPasteRequest() // A fresh presentation must never resurrect a reply prepared or shown by an // earlier one (e.g. a capture send that raced the previous hide). self.replyBinding.clear() @@ -278,7 +289,9 @@ final class QuickChatController: NSObject, NSWindowDelegate { guard self.isVisible else { return } // System permission dialogs steal key focus mid-grant; the bar must survive that flow. guard !self.model.isGrantingPermissions, + !self.model.isStartingDictation, !self.model.isCapturingTextContext, + !self.replyBinding.isPastingReply, self.windowPicker?.isInteractionActive != true, !self.isMenuActive else { return } @@ -286,6 +299,8 @@ final class QuickChatController: NSObject, NSWindowDelegate { } private func dismiss(immediate: Bool) { + self.stopDictation() + self.cancelPasteRequest() if self.isVisible { quickChatLogger.info("quick chat dismiss immediate=\(immediate)") } @@ -361,9 +376,18 @@ final class QuickChatController: NSObject, NSWindowDelegate { onShowAgentPicker: { [weak self] in self?.showAgentPicker() }, + onShowModelMenu: { [weak self] in + self?.showModelMenu() + }, onShowRecentSessions: { [weak self] in self?.showRecentSessionsPicker() }, + onToggleDictation: { [weak self] in + self?.toggleDictation() + }, + onStopDictation: { [weak self] in + self?.stopDictation() + }, onCaptureTextContext: { [weak self] in self?.captureFocusedAppText() }, @@ -373,6 +397,9 @@ final class QuickChatController: NSObject, NSWindowDelegate { onGrantPermissions: { [weak self] in self?.grantMissingPermissions() }, + onPasteReply: { [weak self] in + self?.pasteReplyToFrontmostApp() + }, onContentHeightChange: { [weak self] height in self?.updateContentHeight(height) }, @@ -429,6 +456,205 @@ final class QuickChatController: NSObject, NSWindowDelegate { } } + private func toggleDictation() { + if self.model.isDictating || self.model.isStartingDictation { + self.stopDictation() + return + } + guard self.isVisible, let textView = self.textView, + self.model.prepareDictation(selection: textView.selectedRange()) + else { return } + + let requestID = UUID() + let presentationID = self.model.activePresentationID + self.dictationRequestID = requestID + self.dictationStartTask?.cancel() + self.dictationStartTask = Task { [weak self] in + guard let self else { return } + let permissions = await PermissionManager.ensure( + [.microphone, .speechRecognition], + interactive: true) + guard !Task.isCancelled, + self.dictationRequestID == requestID, + self.model.activePresentationID == presentationID, + self.isVisible + else { return } + guard permissions[.microphone] == true, permissions[.speechRecognition] == true else { + self.model.failDictation(message: String( + localized: "Microphone and speech recognition permission required.")) + self.dictationStartTask = nil + self.dismissIfFocusWasLost() + return + } + guard self.panel?.isKeyWindow == true else { + // Permission UI can hide a later app switch from the normal resign handler. + // Never start audio unless this exact presentation has regained keyboard focus. + self.dismiss() + return + } + + self.model.dictationDidStart() + do { + try await self.dictation.start { [weak self] event in + guard let self, + self.dictationRequestID == requestID, + self.isVisible, + self.model.isDictating + else { return } + switch event { + case let .transcript(transcript): + self.model.applyDictationTranscript(transcript) + case .finished: + self.stopDictation() + case .failed: + self.stopDictation() + self.model.failDictation(message: String( + localized: "Dictation stopped because speech recognition failed.")) + } + } + } catch { + guard self.dictationRequestID == requestID else { return } + self.model.failDictation(message: String(localized: "Couldn't start dictation.")) + self.dismissIfFocusWasLost() + } + if self.dictationRequestID == requestID { + self.dictationStartTask = nil + } + } + } + + private func stopDictation() { + self.dictationRequestID = UUID() + self.dictationStartTask?.cancel() + self.dictationStartTask = nil + self.dictation.stop() + self.model.stopDictation() + } + + private func pasteReplyToFrontmostApp() { + guard let viewModel = self.replyBinding.viewModel, + let text = QuickChatPasteLogic.finalAssistantText( + messages: viewModel.messages, + afterUserIdempotencyKey: self.model.lastAcceptedIdempotencyKey, + streamingAssistantText: viewModel.streamingAssistantText, + pendingRunCount: viewModel.pendingRunCount), + self.replyBinding.beginPaste() + else { return } + self.stopDictation() + + guard let targetApp = NSWorkspace.shared.frontmostApplication else { + self.replyBinding.finishPaste(message: String(localized: "No app is available to paste into.")) + return + } + guard QuickChatPasteLogic.canPaste( + frontmostProcessIdentifier: targetApp.processIdentifier, + ownProcessIdentifier: ProcessInfo.processInfo.processIdentifier) + else { + self.replyBinding.finishPaste(message: String(localized: "Choose another app before pasting.")) + return + } + + let requestID = UUID() + let presentationID = self.model.activePresentationID + self.pasteRequestID = requestID + self.pasteTask?.cancel() + self.pasteTask = Task { [weak self, targetApp] in + guard let self else { return } + let permissions = await PermissionManager.ensure([.accessibility], interactive: true) + guard self.isCurrentPasteRequest(requestID, presentationID: presentationID) else { return } + guard permissions[.accessibility] == true else { + self.finishPasteRequest( + requestID, + message: String(localized: "Accessibility permission is required to paste.")) + return + } + + // Match a normal copy/paste operation: replace the general pasteboard and + // deliberately leave the reply there instead of restoring stale clipboard data. + let pasteboard = NSPasteboard.general + pasteboard.clearContents() + guard pasteboard.setString(text, forType: .string), + await self.activatePasteTarget(targetApp) + else { + self.finishPasteRequest( + requestID, + message: String(localized: "Couldn't paste the reply.")) + return + } + let frontmostPID = NSWorkspace.shared.frontmostApplication?.processIdentifier + guard self.isCurrentPasteRequest(requestID, presentationID: presentationID), + QuickChatPasteLogic.isExpectedTarget( + frontmostProcessIdentifier: frontmostPID, + targetProcessIdentifier: targetApp.processIdentifier), + QuickChatPasteEventInjector.postCommandV(to: targetApp.processIdentifier) + else { + if self.isCurrentPasteRequest(requestID, presentationID: presentationID) { + self.finishPasteRequest( + requestID, + message: String(localized: "Couldn't paste the reply.")) + } + return + } + self.finishPasteRequest(requestID) + self.dismiss() + } + } + + private func activatePasteTarget(_ targetApp: NSRunningApplication) async -> Bool { + let targetPID = targetApp.processIdentifier + if NSWorkspace.shared.frontmostApplication?.processIdentifier == targetPID { + return true + } + guard targetApp.activate(options: []) else { return false } + let clock = ContinuousClock() + let deadline = clock.now.advanced(by: .seconds(1)) + while clock.now < deadline { + guard !Task.isCancelled else { return false } + if NSWorkspace.shared.frontmostApplication?.processIdentifier == targetPID { + return true + } + do { + try await Task.sleep(for: .milliseconds(20)) + } catch is CancellationError { + return false + } catch { + return false + } + } + return NSWorkspace.shared.frontmostApplication?.processIdentifier == targetPID + } + + private func isCurrentPasteRequest(_ requestID: UUID, presentationID: UUID?) -> Bool { + !Task.isCancelled && + self.pasteRequestID == requestID && + self.model.activePresentationID == presentationID && + self.replyBinding.isPastingReply && + self.isVisible + } + + private func finishPasteRequest(_ requestID: UUID, message: String? = nil) { + guard self.pasteRequestID == requestID else { return } + self.pasteTask = nil + self.replyBinding.finishPaste(message: message) + if message != nil { + self.dismissIfFocusWasLost() + } + } + + private func dismissIfFocusWasLost() { + guard self.isVisible, self.panel?.isKeyWindow != true else { return } + self.dismiss() + } + + private func cancelPasteRequest() { + self.pasteRequestID = UUID() + self.pasteTask?.cancel() + self.pasteTask = nil + if self.replyBinding.isPastingReply { + self.replyBinding.finishPaste() + } + } + private func installDismissMonitors() { guard self.monitoringEnabled, self.globalMonitor == nil, self.localMonitor == nil else { return } let mouseEvents: NSEvent.EventTypeMask = [.leftMouseDown, .rightMouseDown, .otherMouseDown] @@ -447,7 +673,9 @@ final class QuickChatController: NSObject, NSWindowDelegate { private func dismissIfClickOutside(at point: NSPoint) { guard self.isVisible, !self.model.isGrantingPermissions, + !self.model.isStartingDictation, !self.model.isCapturingTextContext, + !self.replyBinding.isPastingReply, self.windowPicker?.isInteractionActive != true, !self.isMenuActive, let panel = self.panel @@ -499,6 +727,26 @@ final class QuickChatController: NSObject, NSWindowDelegate { } } + private func showModelMenu() { + guard self.model.canUseModelControls, + let panel, + let contentView = panel.contentView + else { return } + + self.isMenuActive = true + self.removeDismissMonitors() + defer { + self.isMenuActive = false + if self.isVisible { self.installDismissMonitors() } + self.focusEditor() + } + + QuickChatModelMenuPresenter.present( + model: self.model, + panel: panel, + contentView: contentView) + } + private func showRecentSessionsPicker() { guard self.canShowRecentSessions, self.recentSessionsTask == nil else { return } let requestID = UUID() @@ -545,6 +793,7 @@ final class QuickChatController: NSObject, NSWindowDelegate { private func grantMissingPermissions() { self.invalidateRecentsFetch() + self.stopDictation() self.model.grantMissingPermissions() } diff --git a/apps/macos/Sources/OpenClaw/QuickChatDictation.swift b/apps/macos/Sources/OpenClaw/QuickChatDictation.swift new file mode 100644 index 00000000000..7116b98dc25 --- /dev/null +++ b/apps/macos/Sources/OpenClaw/QuickChatDictation.swift @@ -0,0 +1,181 @@ +import AVFoundation +import Foundation +import Speech + +struct QuickChatDictationTextUpdate: Equatable, Sendable { + let text: String + let selection: NSRange +} + +struct QuickChatDictationTextSession: Equatable, Sendable { + let baseText: String + let replacementRange: NSRange + + init(baseText: String, replacementRange: NSRange) { + let utf16Count = baseText.utf16.count + let location = min(max(0, replacementRange.location), utf16Count) + let length = min(max(0, replacementRange.length), utf16Count - location) + self.baseText = baseText + self.replacementRange = NSRange(location: location, length: length) + } + + func update(transcript: String) -> QuickChatDictationTextUpdate { + let transcript = transcript.trimmingCharacters(in: .whitespacesAndNewlines) + guard !transcript.isEmpty, + let range = Range(self.replacementRange, in: self.baseText) + else { + return QuickChatDictationTextUpdate( + text: self.baseText, + selection: NSRange(location: self.replacementRange.location, length: 0)) + } + + let insertion = self.spacedInsertion(transcript, at: range) + let text = self.baseText.replacingCharacters(in: range, with: insertion) + return QuickChatDictationTextUpdate( + text: text, + selection: NSRange( + location: self.replacementRange.location + insertion.utf16.count, + length: 0)) + } + + private func spacedInsertion(_ transcript: String, at range: Range) -> String { + guard self.replacementRange.length == 0 else { return transcript } + let prefix = self.baseText[.. Bool { + character.unicodeScalars.allSatisfy { CharacterSet.punctuationCharacters.contains($0) } + } +} + +/// Short-lived speech recognition dedicated to the Quick Chat composer. +/// A private serial queue owns every Speech/AVFoundation object so audio work +/// stays off the main actor and `stop()` can synchronously release the mic. +final class QuickChatDictation: @unchecked Sendable { + enum Event: Sendable { + case transcript(String) + case finished + case failed + } + + typealias UpdateHandler = @MainActor @Sendable (Event) -> Void + + private let queue = DispatchQueue(label: "ai.openclaw.quickchat.dictation") + private var recognizer: SFSpeechRecognizer? + private var audioEngine: AVAudioEngine? + private var recognitionRequest: SFSpeechAudioBufferRecognitionRequest? + private var recognitionTask: SFSpeechRecognitionTask? + private var tapInstalled = false + private var sessionID = UUID() + + func start(onUpdate: @escaping UpdateHandler) async throws { + try await withCheckedThrowingContinuation { continuation in + self.queue.async { + do { + try self.startLocked(onUpdate: onUpdate) + continuation.resume() + } catch { + self.stopLocked() + continuation.resume(throwing: error) + } + } + } + } + + func stop() { + self.queue.sync { + self.stopLocked() + } + } + + private func startLocked(onUpdate: @escaping UpdateHandler) throws { + self.stopLocked() + let sessionID = UUID() + self.sessionID = sessionID + + let recognizer = SFSpeechRecognizer(locale: Locale(identifier: Locale.current.identifier)) + guard let recognizer, recognizer.isAvailable else { + throw NSError( + domain: "QuickChatDictation", + code: 1, + userInfo: [NSLocalizedDescriptionKey: "Recognizer unavailable"]) + } + self.recognizer = recognizer + + let request = SFSpeechAudioBufferRecognitionRequest() + request.shouldReportPartialResults = true + self.recognitionRequest = request + + guard AudioInputDeviceObserver.hasUsableDefaultInputDevice() else { + throw NSError( + domain: "QuickChatDictation", + code: 2, + userInfo: [NSLocalizedDescriptionKey: "No usable audio input device available"]) + } + + let audioEngine = AVAudioEngine() + self.audioEngine = audioEngine + let input = audioEngine.inputNode + let format = input.outputFormat(forBus: 0) + input.installTap(onBus: 0, bufferSize: 2048, format: format) { [weak request] buffer, _ in + request?.append(SpeechAudioBufferNormalizer.speechCompatibleBuffer(from: buffer)) + } + self.tapInstalled = true + + audioEngine.prepare() + try audioEngine.start() + + self.recognitionTask = recognizer.recognitionTask(with: request) { [weak self] result, error in + guard let self else { return } + let transcript = result?.bestTranscription.formattedString + let isFinal = result?.isFinal ?? false + self.queue.async { [weak self] in + guard let self, self.sessionID == sessionID else { return } + if error != nil || isFinal { + self.stopLocked() + } + Task { @MainActor in + if let transcript { + onUpdate(.transcript(transcript)) + } + if error != nil { + onUpdate(.failed) + } else if isFinal { + onUpdate(.finished) + } + } + } + } + } + + private func stopLocked() { + self.sessionID = UUID() + if self.tapInstalled { + self.audioEngine?.inputNode.removeTap(onBus: 0) + self.tapInstalled = false + } + self.recognitionRequest?.endAudio() + self.recognitionTask?.cancel() + self.recognitionTask = nil + self.recognitionRequest = nil + self.recognizer = nil + if self.audioEngine?.isRunning == true { + self.audioEngine?.stop() + self.audioEngine?.reset() + } + self.audioEngine = nil + } +} diff --git a/apps/macos/Sources/OpenClaw/QuickChatModel.swift b/apps/macos/Sources/OpenClaw/QuickChatModel.swift index 18735de762a..864b3aeb56e 100644 --- a/apps/macos/Sources/OpenClaw/QuickChatModel.swift +++ b/apps/macos/Sources/OpenClaw/QuickChatModel.swift @@ -105,6 +105,7 @@ final class QuickChatModel { private struct RetryIdentity { let draft: String let message: String + let thinking: String? let sessionKey: String let agentID: String? let attachments: [OpenClawChatAttachmentPayload] @@ -118,6 +119,7 @@ final class QuickChatModel { String, String?, String, + String?, String, [OpenClawChatAttachmentPayload]) async throws -> String typealias PermissionStatusProvider = @MainActor ([Capability]) async -> [Capability: Bool] @@ -125,6 +127,11 @@ final class QuickChatModel { typealias ConnectionGateProvider = @MainActor () -> QuickChatConnectionGate typealias FrontmostAppNameProvider = @MainActor () -> String typealias TextContextCaptureProvider = @MainActor () async -> QuickChatTextContextCaptureOutcome + typealias ModelControlsProvider = @MainActor ( + QuickChatRoutingTarget) async throws -> QuickChatModelControlSnapshot + typealias ModelPatchProvider = @MainActor ( + QuickChatRoutingTarget, + String?) async throws -> OpenClawChatModelPatchResult? static let trackedPermissions: [Capability] = [.notifications, .accessibility, .screenRecording] @@ -136,6 +143,9 @@ final class QuickChatModel { if self.sendTask == nil, let retryIdentity = self.retryIdentity, retryIdentity.draft != self.text { self.retryIdentity = nil } + if !self.isDictating, !self.isStartingDictation, self.dictationTextSession == nil { + self.dictationSelectionRange = nil + } } } @@ -155,9 +165,24 @@ final class QuickChatModel { private(set) var textContext: QuickChatTextContext? private(set) var isCapturingTextContext = false private(set) var textContextCaptureMessage: String? + private(set) var isStartingDictation = false + private(set) var isDictating = false + private(set) var dictationStatusMessage: String? + private(set) var dictationSelectionRange: NSRange? + private(set) var modelChoices: [OpenClawChatModelChoice] = [] + private(set) var currentSessionModelSelectionID: String? + private(set) var currentSessionThinkingLevel: String? + private(set) var thinkingOptions = QuickChatModelControlLogic.baseThinkingOptions + private(set) var selectedModelSelectionID: String? + private(set) var selectedThinkingLevel: String? + private(set) var modelDefaultProvider: String? + private(set) var isLoadingModelControls = false + private(set) var isUpdatingModel = false + private(set) var modelControlStatusMessage: String? /// Route of the most recently accepted send; navigation reads this immutable value /// instead of sampling live routing state that an agent switch could move meanwhile. private(set) var lastAcceptedRoute: QuickChatRoutingTarget? + private(set) var lastAcceptedIdempotencyKey: String? @ObservationIgnored private let sessionKeyProvider: SessionKeyProvider @ObservationIgnored private let agentsProvider: AgentsProvider @@ -168,6 +193,8 @@ final class QuickChatModel { @ObservationIgnored private let connectionGateProvider: ConnectionGateProvider @ObservationIgnored private let frontmostAppNameProvider: FrontmostAppNameProvider @ObservationIgnored private let textContextCaptureProvider: TextContextCaptureProvider + @ObservationIgnored private let modelControlsProvider: ModelControlsProvider + @ObservationIgnored private let modelPatchProvider: ModelPatchProvider /// Invoked with the snapshotted route just before a send is dispatched, for every /// send path (text and capture); wires the reply consumer's pre-bind. @ObservationIgnored var onSendDispatched: ((QuickChatRoutingTarget) -> Void)? @@ -182,6 +209,14 @@ final class QuickChatModel { @ObservationIgnored private var capturePipelineID: UUID? @ObservationIgnored private var textContextCaptureID: UUID? @ObservationIgnored private var textContextCaptureTask: Task? + @ObservationIgnored private var dictationTextSession: QuickChatDictationTextSession? + @ObservationIgnored private var modelControlsTask: Task? + @ObservationIgnored private var modelControlsRequestID = UUID() + @ObservationIgnored private var modelPatchTask: Task? + @ObservationIgnored private var modelPatchTarget: QuickChatRoutingTarget? + @ObservationIgnored private var modelPatchSelectionID: String? + @ObservationIgnored private var modelPatchRequestID = UUID() + @ObservationIgnored private var appliedModelSelections: [QuickChatRoutingTarget: String] = [:] init( sessionKeyProvider: @escaping SessionKeyProvider = { @@ -200,12 +235,12 @@ final class QuickChatModel { emoji: emoji, avatar: QuickChatAgentDisplay.avatar(fromRendered: identity.avatar)) }, - sendProvider: @escaping SendProvider = { sessionKey, agentID, message, idempotencyKey, attachments in + sendProvider: @escaping SendProvider = { sessionKey, agentID, message, thinking, idempotencyKey, attachments in let response = try await GatewayConnection.shared.chatSend( sessionKey: sessionKey, agentID: agentID, message: message, - thinking: nil, + thinking: thinking, idempotencyKey: idempotencyKey, attachments: attachments) return response.status @@ -228,6 +263,24 @@ final class QuickChatModel { }, textContextCaptureProvider: @escaping TextContextCaptureProvider = { await QuickChatFocusedTextCaptureService.capture() + }, + modelControlsProvider: @escaping ModelControlsProvider = { target in + let transport = MacGatewayChatTransport(defaultGlobalAgentID: target.agentID) + async let models = transport.listModels() + async let sessions = transport.listSessions(limit: 200, search: target.sessionKey, archived: false) + async let agents = GatewayConnection.shared.agentsList() + return try await QuickChatModelControlLogic.snapshot( + target: target, + models: models, + sessions: sessions, + agents: agents) + }, + modelPatchProvider: @escaping ModelPatchProvider = { target, model in + let transport = MacGatewayChatTransport(defaultGlobalAgentID: target.agentID) + return try await transport.patchSessionSettings( + sessionKey: target.sessionKey, + agentID: target.agentID, + patch: OpenClawChatSessionSettingsPatch(model: .some(model))) }) { self.sessionKeyProvider = sessionKeyProvider @@ -239,6 +292,8 @@ final class QuickChatModel { self.connectionGateProvider = connectionGateProvider self.frontmostAppNameProvider = frontmostAppNameProvider self.textContextCaptureProvider = textContextCaptureProvider + self.modelControlsProvider = modelControlsProvider + self.modelPatchProvider = modelPatchProvider } var connectionGate: QuickChatConnectionGate { @@ -259,10 +314,15 @@ final class QuickChatModel { } var canSend: Bool { - (!self.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || self.textContext != nil) && + let hasPendingOverrideState = self.selectedModelSelectionID != nil || self.selectedThinkingLevel != nil + return (!self.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || self.textContext != nil) && !self.sessionKey.isEmpty && self.connectionGate == .available && !self.isCapturingTextContext && + !self.isStartingDictation && + !self.isUpdatingModel && + (!hasPendingOverrideState || !self.isLoadingModelControls) && + self.isSelectedThinkingLevelSupported && self.sendState != .sending } @@ -271,6 +331,9 @@ final class QuickChatModel { self.connectionGate == .available && !self.isGrantingPermissions && !self.isCapturingTextContext && + !self.isStartingDictation && + !self.isDictating && + !self.isUpdatingModel && self.sendState != .sending } @@ -283,9 +346,22 @@ final class QuickChatModel { self.connectionGate == .available && !self.isGrantingPermissions && !self.isCapturingTextContext && + !self.isStartingDictation && + !self.isDictating && + !self.isUpdatingModel && self.sendState != .sending } + var canToggleDictation: Bool { + self.isDictating || self.isStartingDictation || ( + !self.sessionKey.isEmpty && + self.connectionGate == .available && + !self.isGrantingPermissions && + !self.isCapturingTextContext && + !self.isUpdatingModel && + self.sendState != .sending) + } + var messagePlaceholder: String { if let targetSessionOverride { return "Reply in \(targetSessionOverride.displayName)" @@ -311,6 +387,14 @@ final class QuickChatModel { self.textContext = nil self.textContextCaptureMessage = nil self.cancelTextContextCapture() + self.resetDictationState() + self.cancelModelControlRefresh() + self.appliedModelSelections.removeAll() + self.selectedModelSelectionID = nil + self.selectedThinkingLevel = nil + self.currentSessionModelSelectionID = nil + self.currentSessionThinkingLevel = nil + self.modelControlStatusMessage = nil self.targetSessionOverride = nil self.baseRoutingTarget = nil // The cached list stays displayable, but routing metadata must wait for the fresh @@ -349,6 +433,7 @@ final class QuickChatModel { // A held or in-flight send is already routed; switching now would reroute a // screenshot captured for the previously selected agent. guard self.sendState != .sending, + !self.isUpdatingModel, let display = self.agents.first(where: { $0.id == id }), let mainKey = self.agentsMainKey else { return } @@ -363,7 +448,7 @@ final class QuickChatModel { } func selectSessionOverride(_ target: QuickChatSessionTargetOverride?) { - guard self.sendState != .sending else { return } + guard self.sendState != .sending, !self.isUpdatingModel else { return } self.targetSessionOverride = target self.applyRoutingTarget() } @@ -389,6 +474,69 @@ final class QuickChatModel { } } + func prepareDictation(selection: NSRange) -> Bool { + guard self.canToggleDictation, !self.isDictating, !self.isStartingDictation else { return false } + self.dictationTextSession = QuickChatDictationTextSession( + baseText: self.text, + replacementRange: selection) + self.dictationSelectionRange = nil + self.dictationStatusMessage = nil + self.isStartingDictation = true + return true + } + + func dictationDidStart() { + guard self.isStartingDictation else { return } + self.isStartingDictation = false + self.isDictating = true + } + + func applyDictationTranscript(_ transcript: String) { + guard self.isDictating, let dictationTextSession else { return } + let update = dictationTextSession.update(transcript: transcript) + self.dictationSelectionRange = update.selection + self.text = update.text + } + + func stopDictation() { + self.isStartingDictation = false + self.isDictating = false + self.dictationTextSession = nil + } + + func failDictation(message: String) { + self.stopDictation() + self.dictationStatusMessage = message + } + + func selectThinkingLevel(_ level: String?) { + let normalized = level?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + let selection = normalized?.isEmpty == false ? normalized : nil + guard selection != self.selectedThinkingLevel else { return } + self.selectedThinkingLevel = selection + self.modelControlStatusMessage = nil + self.retryIdentity = nil + } + + func selectModel(_ selectionID: String) { + guard self.canUseModelControls, !self.isUpdatingModel, let target = self.routingTarget else { return } + let normalized = selectionID.trimmingCharacters(in: .whitespacesAndNewlines) + guard normalized == OpenClawChatViewModel.defaultModelSelectionID || + self.modelChoices.contains(where: { $0.selectionID == normalized }) + else { return } + guard normalized != self.selectedModelSelectionID else { return } + let patchDecision = QuickChatModelControlLogic.modelPatchDecision( + selectionID: normalized, + appliedSelectionID: self.appliedModelSelections[target], + currentSessionSelectionID: self.currentSessionModelSelectionID) + self.selectedModelSelectionID = normalized + self.modelControlStatusMessage = nil + if patchDecision != .none { + self.retryIdentity = nil + } + self.startSelectedModelPatchIfNeeded(selectionID: normalized, target: target) + } + func send() async -> Bool { await self.performSend(messageOverride: nil, attachments: []) } @@ -586,6 +734,16 @@ final class QuickChatModel { self.clearTextContext() self.textContextCaptureMessage = nil self.cancelTextContextCapture() + self.resetDictationState() + // Model patches are persistent session mutations. Let an accepted selection + // settle even though its presentation-scoped controls are being discarded. + self.cancelModelControlRefresh() + self.appliedModelSelections.removeAll() + self.selectedModelSelectionID = nil + self.selectedThinkingLevel = nil + self.currentSessionModelSelectionID = nil + self.currentSessionThinkingLevel = nil + self.modelControlStatusMessage = nil // A dispatched chat.send may already be accepted; cancelling and retrying with a new UUID can duplicate it. self.cancelPermissionTask() self.cancelPermissionPolling() @@ -598,6 +756,8 @@ final class QuickChatModel { self.cancelPermissionTask() self.cancelPermissionPolling() self.cancelTextContextCapture() + self.cancelModelControlRefresh() + self.resetDictationState() if self.sendState == .sending { self.sendState = .idle } } @@ -665,11 +825,22 @@ final class QuickChatModel { guard let baseRoutingTarget else { self.sessionKey = "" self.sendAgentID = nil + self.cancelModelControlRefresh() return } let target = Self.routingTarget(override: self.targetSessionOverride, base: baseRoutingTarget) + let previousTarget = self.routingTarget self.sessionKey = target.sessionKey self.sendAgentID = target.agentID + if previousTarget != target { + // Explicit overrides survive target changes, but the target's underlying + // session state must be read again before the compact control claims a value. + self.currentSessionModelSelectionID = nil + self.currentSessionThinkingLevel = nil + self.modelDefaultProvider = nil + self.thinkingOptions = [] + } + self.refreshModelControls(for: target) } private func performSend( @@ -679,6 +850,7 @@ final class QuickChatModel { textContextOverride: QuickChatTextContext? = nil, continuesCapturePipeline: Bool = false) async -> Bool { + guard let presentationID = self.activePresentationID else { return false } // The capture pipeline captured its draft before detached processing; edits made // meanwhile must survive, so the clear-decision compares against that original. let draft = draftOverride ?? self.text @@ -692,13 +864,29 @@ final class QuickChatModel { let sessionKey = self.sessionKey let agentID = self.sendAgentID + let route = QuickChatRoutingTarget(sessionKey: sessionKey, agentID: agentID) + let selectedModelSelectionID = self.selectedModelSelectionID + guard await self.applySelectedModelIfNeeded( + to: route, + selectionID: selectedModelSelectionID) + else { return false } + guard !Task.isCancelled, + self.isCurrentPresentation(presentationID), + self.routingTarget == route, + self.connectionGate == .available, + self.isSelectedThinkingLevelSupported, + continuesCapturePipeline || self.sendState != .sending + else { return false } + let thinking = self.selectedThinkingLevel + self.lastAcceptedIdempotencyKey = nil // Pre-bind the reply consumer for every send path (text and screenshots): a // fast turn must not emit frames before the reply view model starts listening. - self.onSendDispatched?(QuickChatRoutingTarget(sessionKey: sessionKey, agentID: agentID)) + self.onSendDispatched?(route) let idempotencyKey: String if let retryIdentity = self.retryIdentity, retryIdentity.draft == draft, retryIdentity.message == message, + retryIdentity.thinking == thinking, retryIdentity.sessionKey == sessionKey, retryIdentity.agentID == agentID, retryIdentity.attachments == attachments @@ -709,13 +897,14 @@ final class QuickChatModel { self.retryIdentity = RetryIdentity( draft: draft, message: message, + thinking: thinking, sessionKey: sessionKey, agentID: agentID, attachments: attachments, idempotencyKey: idempotencyKey) } let task = Task { - try await self.sendProvider(sessionKey, agentID, message, idempotencyKey, attachments) + try await self.sendProvider(sessionKey, agentID, message, thinking, idempotencyKey, attachments) } self.sendTask = task self.sendState = .sending @@ -732,7 +921,8 @@ final class QuickChatModel { return false case .terminalSuccess, .inFlight: self.retryIdentity = nil - self.lastAcceptedRoute = QuickChatRoutingTarget(sessionKey: sessionKey, agentID: agentID) + self.lastAcceptedRoute = route + self.lastAcceptedIdempotencyKey = idempotencyKey if self.textContext == textContext { self.textContext = nil } @@ -802,6 +992,248 @@ final class QuickChatModel { } } +extension QuickChatModel { + var canUseModelControls: Bool { + !self.sessionKey.isEmpty && + self.connectionGate == .available && + !self.isGrantingPermissions && + !self.isCapturingTextContext && + !self.isStartingDictation && + !self.isLoadingModelControls && + !self.isUpdatingModel && + self.sendState != .sending + } + + var isSelectedThinkingLevelSupported: Bool { + QuickChatModelControlLogic.validatedThinkingSelection( + self.selectedThinkingLevel, + options: self.thinkingOptions) == self.selectedThinkingLevel + } + + var modelPickerSections: ChatModelPickerSections { + ChatModelPickerStore.sections( + choices: self.modelChoices, + favorites: [], + recents: [], + defaultProvider: self.modelDefaultProvider) + } + + var displayedModelSelectionID: String? { + self.selectedModelSelectionID ?? self.currentSessionModelSelectionID + } + + var displayedThinkingLevel: String? { + self.selectedThinkingLevel ?? self.currentSessionThinkingLevel + } + + var modelControlLabel: String { + let automatic = String(localized: "Auto") + let model = QuickChatModelControlLogic.displayName( + selectionID: self.displayedModelSelectionID, + models: self.modelChoices, + automaticLabel: automatic) + let thinking = self.thinkingOptions.first(where: { $0.id == self.displayedThinkingLevel })?.label + ?? self.displayedThinkingLevel + ?? automatic + return String(localized: "\(model) · \(thinking)") + } + + private func refreshModelControls(for target: QuickChatRoutingTarget) { + guard self.isPresentationActive else { return } + let requestID = UUID() + self.modelControlsRequestID = requestID + self.modelControlsTask?.cancel() + self.isLoadingModelControls = true + self.modelControlsTask = Task { [weak self] in + guard let self else { return } + do { + let snapshot = try await self.modelControlsProvider(target) + guard !Task.isCancelled, + self.modelControlsRequestID == requestID, + self.routingTarget == target + else { return } + self.modelChoices = snapshot.models + self.currentSessionModelSelectionID = snapshot.currentModelSelectionID + self.currentSessionThinkingLevel = snapshot.currentThinkingLevel + self.thinkingOptions = snapshot.thinkingOptions + self.modelDefaultProvider = snapshot.defaultProvider + self.modelControlStatusMessage = self.isSelectedThinkingLevelSupported + ? nil + : String(localized: "Selected reasoning is unavailable for this target.") + } catch is CancellationError { + return + } catch { + guard self.modelControlsRequestID == requestID, + self.routingTarget == target + else { return } + self.modelControlStatusMessage = String(localized: "Couldn't load model settings.") + } + guard self.modelControlsRequestID == requestID else { return } + self.isLoadingModelControls = false + self.modelControlsTask = nil + } + } + + private func startSelectedModelPatchIfNeeded(selectionID: String, target: QuickChatRoutingTarget) { + Task { [weak self] in + _ = await self?.applySelectedModelIfNeeded(to: target, selectionID: selectionID) + } + } + + private func applySelectedModelIfNeeded( + to target: QuickChatRoutingTarget, + selectionID: String?) async -> Bool + { + let appliedSelectionID = self.appliedModelSelections[target] + switch QuickChatModelControlLogic.modelPatchDecision( + selectionID: selectionID, + appliedSelectionID: appliedSelectionID, + currentSessionSelectionID: self.routingTarget == target + ? self.currentSessionModelSelectionID + : nil) + { + case .none: + return true + case let .patch(model): + // Model is a session setting and persists through sessions.patch. Reasoning + // remains a per-message override passed only to chat.send below. + if let task = self.modelPatchTask, + self.modelPatchTarget == target, + self.modelPatchSelectionID == selectionID + { + return await self.finishModelPatch( + task, + requestID: self.modelPatchRequestID, + target: target, + selectionID: selectionID) + } + + self.modelPatchTask?.cancel() + self.modelControlsRequestID = UUID() + self.modelControlsTask?.cancel() + self.modelControlsTask = nil + self.isLoadingModelControls = false + let requestID = UUID() + self.modelPatchRequestID = requestID + self.modelPatchTarget = target + self.modelPatchSelectionID = selectionID + self.isUpdatingModel = true + let task = Task { + try await self.modelPatchProvider(target, model) + } + self.modelPatchTask = task + return await self.finishModelPatch( + task, + requestID: requestID, + target: target, + selectionID: selectionID) + } + } + + private func finishModelPatch( + _ task: Task, + requestID: UUID, + target: QuickChatRoutingTarget, + selectionID: String?) async -> Bool + { + do { + let result = try await task.value + var refreshedSnapshot: QuickChatModelControlSnapshot? + var refreshFailed = false + if result?.thinkingLevels == nil, self.routingTarget == target { + do { + refreshedSnapshot = try await self.modelControlsProvider(target) + } catch { + refreshFailed = true + } + } + guard self.modelPatchRequestID == requestID else { return false } + if self.isPresentationActive, let selectionID { + self.appliedModelSelections[target] = selectionID + } + if self.routingTarget == target { + self.modelControlsRequestID = UUID() + self.modelControlsTask?.cancel() + self.modelControlsTask = nil + self.isLoadingModelControls = false + if let refreshedSnapshot { + self.modelChoices = refreshedSnapshot.models + self.currentSessionModelSelectionID = refreshedSnapshot.currentModelSelectionID + self.currentSessionThinkingLevel = refreshedSnapshot.currentThinkingLevel + self.modelDefaultProvider = refreshedSnapshot.defaultProvider + } else if let model = result?.model { + self.currentSessionModelSelectionID = QuickChatModelControlLogic.selectionID( + model: model, + provider: result?.modelProvider) + } else if selectionID != OpenClawChatViewModel.defaultModelSelectionID { + self.currentSessionModelSelectionID = selectionID + } else { + self.currentSessionModelSelectionID = nil + } + // The refreshedSnapshot branch above already seeded currentSessionThinkingLevel + // from the authoritative fetch, so when the patch response omits both + // thinkingLevels and thinkingLevel both branches here are skipped and the + // refreshed level survives; these only override when the patch itself + // returned a level. + if result?.thinkingLevels != nil { + self.currentSessionThinkingLevel = result?.thinkingLevel + } else if let thinkingLevel = result?.thinkingLevel { + self.currentSessionThinkingLevel = thinkingLevel + } + let thinkingOptions = result?.thinkingLevels ?? refreshedSnapshot?.thinkingOptions ?? [] + self.thinkingOptions = thinkingOptions + if !refreshFailed { + let validated = QuickChatModelControlLogic.validatedThinkingSelection( + self.selectedThinkingLevel, + options: thinkingOptions) + if validated != self.selectedThinkingLevel { + self.selectedThinkingLevel = validated + self.retryIdentity = nil + } + } + self.modelControlStatusMessage = refreshFailed + ? String(localized: "Couldn't load model settings.") + : nil + } + self.clearModelPatchState(requestID: requestID) + return true + } catch is CancellationError { + self.clearModelPatchState(requestID: requestID) + return false + } catch { + guard self.modelPatchRequestID == requestID else { return false } + if self.routingTarget == target { + self.modelControlStatusMessage = String(localized: "Couldn't change the model.") + } + self.clearModelPatchState(requestID: requestID) + return false + } + } + + private func clearModelPatchState(requestID: UUID) { + guard self.modelPatchRequestID == requestID else { return } + self.isUpdatingModel = false + self.modelPatchTask = nil + self.modelPatchTarget = nil + self.modelPatchSelectionID = nil + } + + private func cancelModelControlRefresh() { + self.modelControlsRequestID = UUID() + self.modelControlsTask?.cancel() + self.modelControlsTask = nil + self.isLoadingModelControls = false + } + + private func resetDictationState() { + self.isStartingDictation = false + self.isDictating = false + self.dictationStatusMessage = nil + self.dictationSelectionRange = nil + self.dictationTextSession = nil + } +} + extension String { fileprivate var nonEmptyTrimmed: String? { let trimmed = self.trimmingCharacters(in: .whitespacesAndNewlines) diff --git a/apps/macos/Sources/OpenClaw/QuickChatModelControls.swift b/apps/macos/Sources/OpenClaw/QuickChatModelControls.swift new file mode 100644 index 00000000000..536a2c182ea --- /dev/null +++ b/apps/macos/Sources/OpenClaw/QuickChatModelControls.swift @@ -0,0 +1,163 @@ +import Foundation +import OpenClawChatUI +import OpenClawProtocol + +struct QuickChatModelControlSnapshot: Sendable { + let models: [OpenClawChatModelChoice] + let currentModelSelectionID: String? + let currentThinkingLevel: String? + let thinkingOptions: [OpenClawChatThinkingLevelOption] + let defaultProvider: String? +} + +enum QuickChatModelPatchDecision: Equatable { + case none + case patch(String?) +} + +enum QuickChatModelControlLogic { + static let baseThinkingOptions = ["off", "minimal", "low", "medium", "high"].map { + OpenClawChatThinkingLevelOption(id: $0, label: $0) + } + + static func snapshot( + target: QuickChatRoutingTarget, + models: [OpenClawChatModelChoice], + sessions: OpenClawChatSessionsListResponse, + agents: AgentsListResult?) -> QuickChatModelControlSnapshot + { + let entry = self.sessionEntry(target: target, sessions: sessions.sessions) + let agent = self.agent(target: target, agents: agents) + let entryProvider = self.normalized(entry?.modelProvider) + let entryModel = self.normalized(entry?.model) + let agentModel = self.normalized(agent?.model?["primary"]?.value as? String) + let defaultProvider = self.normalized(sessions.defaults?.modelProvider) + let defaultModel = self.normalized(sessions.defaults?.model) + let selectionID = entryModel.map { + self.selectionID(model: $0, provider: entryProvider ?? defaultProvider) + } ?? agentModel ?? defaultModel.map { + self.selectionID(model: $0, provider: defaultProvider) + } + let thinkingLevel = self.normalized(entry?.thinkingLevel) ?? + self.normalized(entry?.thinkingDefault) ?? + self.normalized(agent?.thinkingdefault) ?? + self.normalized(sessions.defaults?.thinkingDefault) + let thinkingOptions = self.thinkingOptions( + entry: entry, + agent: agent, + defaults: sessions.defaults) + return QuickChatModelControlSnapshot( + models: models, + currentModelSelectionID: selectionID, + currentThinkingLevel: thinkingLevel, + thinkingOptions: thinkingOptions, + defaultProvider: self.provider(selectionID: selectionID)) + } + + static func modelPatchDecision( + selectionID: String?, + appliedSelectionID: String?, + currentSessionSelectionID: String? = nil) -> QuickChatModelPatchDecision + { + guard let selectionID else { return .none } + if selectionID == OpenClawChatViewModel.defaultModelSelectionID { + guard selectionID != appliedSelectionID else { return .none } + return .patch(nil) + } + if selectionID == currentSessionSelectionID { return .none } + if currentSessionSelectionID != nil { return .patch(selectionID) } + guard selectionID != appliedSelectionID else { return .none } + return .patch(selectionID) + } + + static func displayName( + selectionID: String?, + models: [OpenClawChatModelChoice], + automaticLabel: String) -> String + { + guard let selectionID, + selectionID != OpenClawChatViewModel.defaultModelSelectionID + else { return automaticLabel } + if let choice = models.first(where: { $0.selectionID == selectionID }) { + return choice.name + } + return selectionID.split(separator: "/", maxSplits: 1).last.map(String.init) ?? selectionID + } + + static func validatedThinkingSelection( + _ selection: String?, + options: [OpenClawChatThinkingLevelOption]) -> String? + { + guard let selection else { return nil } + let normalized = selection.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + return options.contains(where: { + $0.id.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == normalized + }) ? normalized : nil + } + + private static func sessionEntry( + target: QuickChatRoutingTarget, + sessions: [OpenClawChatSessionEntry]) -> OpenClawChatSessionEntry? + { + let key = target.sessionKey.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + if let exact = sessions.first(where: { $0.key.lowercased() == key }) { + return exact + } + guard key == "global", + let agentID = self.normalized(target.agentID)?.lowercased() + else { return nil } + return sessions.first(where: { $0.key.lowercased() == "agent:\(agentID):global" }) + } + + private static func agent(target: QuickChatRoutingTarget, agents: AgentsListResult?) -> AgentSummary? { + guard let agents else { return nil } + let targetAgentID = self.normalized(target.agentID) ?? + OpenClawChatSessionKey.agentID(from: target.sessionKey) ?? + self.normalized(agents.defaultid) + guard let targetAgentID else { return nil } + return agents.agents.first(where: { $0.id.caseInsensitiveCompare(targetAgentID) == .orderedSame }) + } + + static func selectionID(model: String, provider: String?) -> String { + guard let provider else { return model } + let prefix = "\(provider)/" + return model.hasPrefix(prefix) ? model : "\(prefix)\(model)" + } + + private static func thinkingOptions( + entry: OpenClawChatSessionEntry?, + agent: AgentSummary?, + defaults: OpenClawChatSessionsDefaults?) -> [OpenClawChatThinkingLevelOption] + { + let agentOptions = agent?.thinkinglevels?.compactMap { option -> OpenClawChatThinkingLevelOption? in + guard let id = self.normalized(option["id"]?.value as? String) else { return nil } + let label = self.normalized(option["label"]?.value as? String) ?? id + return OpenClawChatThinkingLevelOption(id: id, label: label) + } + let options = entry?.thinkingLevels ?? + entry?.thinkingOptions?.map { OpenClawChatThinkingLevelOption(id: $0, label: $0) } ?? + agentOptions ?? + defaults?.thinkingLevels ?? + defaults?.thinkingOptions?.map { OpenClawChatThinkingLevelOption(id: $0, label: $0) } ?? + self.baseThinkingOptions + var seen = Set() + return options.compactMap { option in + guard let id = self.normalized(option.id)?.lowercased(), seen.insert(id).inserted else { return nil } + let label = self.normalized(option.label) ?? id + return OpenClawChatThinkingLevelOption(id: id, label: label) + } + } + + private static func provider(selectionID: String?) -> String? { + guard let selectionID, + let separator = selectionID.firstIndex(of: "/"), + separator != selectionID.startIndex + else { return nil } + return String(selectionID[.. String? { + let value = value?.trimmingCharacters(in: .whitespacesAndNewlines) + return value?.isEmpty == false ? value : nil + } +} diff --git a/apps/macos/Sources/OpenClaw/QuickChatModelMenu.swift b/apps/macos/Sources/OpenClaw/QuickChatModelMenu.swift new file mode 100644 index 00000000000..14d8fbb8a36 --- /dev/null +++ b/apps/macos/Sources/OpenClaw/QuickChatModelMenu.swift @@ -0,0 +1,109 @@ +import AppKit +import OpenClawChatUI + +@MainActor +private final class QuickChatModelMenuTarget: NSObject { + let onSelectModel: (String) -> Void + let onSelectThinking: (String?) -> Void + + init( + onSelectModel: @escaping (String) -> Void, + onSelectThinking: @escaping (String?) -> Void) + { + self.onSelectModel = onSelectModel + self.onSelectThinking = onSelectThinking + } + + @objc func selectModel(_ sender: NSMenuItem) { + guard let selectionID = sender.representedObject as? String else { return } + self.onSelectModel(selectionID) + } + + @objc func selectThinking(_ sender: NSMenuItem) { + guard let level = sender.representedObject as? String else { return } + self.onSelectThinking(level) + } + + @objc func selectAutomaticThinking(_: NSMenuItem) { + self.onSelectThinking(nil) + } +} + +@MainActor +enum QuickChatModelMenuPresenter { + static func present(model: QuickChatModel, panel: NSPanel, contentView: NSView) { + let target = QuickChatModelMenuTarget( + onSelectModel: { [weak model] selectionID in + model?.selectModel(selectionID) + }, + onSelectThinking: { [weak model] level in + model?.selectThinkingLevel(level) + }) + let menu = NSMenu() + let modelHeader = NSMenuItem( + title: String(localized: "Model"), + action: nil, + keyEquivalent: "") + modelHeader.isEnabled = false + menu.addItem(modelHeader) + + let defaultItem = NSMenuItem( + title: String(localized: "Session default"), + action: #selector(QuickChatModelMenuTarget.selectModel(_:)), + keyEquivalent: "") + defaultItem.target = target + defaultItem.representedObject = OpenClawChatViewModel.defaultModelSelectionID + defaultItem.state = model.selectedModelSelectionID == OpenClawChatViewModel.defaultModelSelectionID + ? .on + : .off + menu.addItem(defaultItem) + + for section in model.modelPickerSections.providers { + let providerItem = NSMenuItem(title: section.displayName, action: nil, keyEquivalent: "") + let submenu = NSMenu() + for choice in section.models { + let item = NSMenuItem( + title: choice.name, + action: #selector(QuickChatModelMenuTarget.selectModel(_:)), + keyEquivalent: "") + item.target = target + item.representedObject = choice.selectionID + item.state = model.displayedModelSelectionID == choice.selectionID ? .on : .off + submenu.addItem(item) + } + providerItem.submenu = submenu + menu.addItem(providerItem) + } + + menu.addItem(.separator()) + let reasoningHeader = NSMenuItem( + title: String(localized: "Reasoning"), + action: nil, + keyEquivalent: "") + reasoningHeader.isEnabled = false + menu.addItem(reasoningHeader) + let automaticItem = NSMenuItem( + title: String(localized: "Auto"), + action: #selector(QuickChatModelMenuTarget.selectAutomaticThinking(_:)), + keyEquivalent: "") + automaticItem.target = target + automaticItem.state = model.selectedThinkingLevel == nil ? .on : .off + menu.addItem(automaticItem) + for option in model.thinkingOptions { + let item = NSMenuItem( + title: option.label, + action: #selector(QuickChatModelMenuTarget.selectThinking(_:)), + keyEquivalent: "") + item.target = target + item.representedObject = option.id + item.state = model.selectedThinkingLevel == option.id ? .on : .off + menu.addItem(item) + } + + let windowPoint = panel.convertPoint(fromScreen: NSEvent.mouseLocation) + let contentPoint = contentView.convert(windowPoint, from: nil) + withExtendedLifetime(target) { + _ = menu.popUp(positioning: nil, at: contentPoint, in: contentView) + } + } +} diff --git a/apps/macos/Sources/OpenClaw/QuickChatPaste.swift b/apps/macos/Sources/OpenClaw/QuickChatPaste.swift new file mode 100644 index 00000000000..adffd008051 --- /dev/null +++ b/apps/macos/Sources/OpenClaw/QuickChatPaste.swift @@ -0,0 +1,68 @@ +import Carbon.HIToolbox +import CoreGraphics +import Foundation +import OpenClawChatUI + +enum QuickChatPasteLogic { + static func finalAssistantText( + messages: [OpenClawChatMessage], + afterUserIdempotencyKey: String?, + streamingAssistantText: String?, + pendingRunCount: Int) -> String? + { + guard streamingAssistantText == nil, + pendingRunCount == 0, + let afterUserIdempotencyKey, + let userIndex = messages.lastIndex(where: { + $0.role.lowercased() == "user" && $0.idempotencyKey == afterUserIdempotencyKey + }), + !messages[(userIndex + 1)...].contains(where: { $0.role.lowercased() == "user" }), + let assistantIndex = messages.indices.reversed().first(where: { + $0 > userIndex && messages[$0].role.lowercased() == "assistant" + }) + else { return nil } + + let lastConversationalIndex = messages.indices.reversed().first(where: { + let role = messages[$0].role.lowercased() + return role == "user" || role == "assistant" + }) + guard lastConversationalIndex == assistantIndex else { return nil } + let text = ChatMessageVisibleText.visibleText(in: messages[assistantIndex]) + return text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? nil : text + } + + static func canPaste( + frontmostProcessIdentifier: Int32?, + ownProcessIdentifier: Int32) -> Bool + { + guard let frontmostProcessIdentifier else { return false } + return frontmostProcessIdentifier != ownProcessIdentifier + } + + static func isExpectedTarget( + frontmostProcessIdentifier: Int32?, + targetProcessIdentifier: Int32) -> Bool + { + frontmostProcessIdentifier == targetProcessIdentifier + } +} + +enum QuickChatPasteEventInjector { + static func postCommandV(to processIdentifier: pid_t) -> Bool { + guard let source = CGEventSource(stateID: .hidSystemState), + let keyDown = CGEvent( + keyboardEventSource: source, + virtualKey: CGKeyCode(kVK_ANSI_V), + keyDown: true), + let keyUp = CGEvent( + keyboardEventSource: source, + virtualKey: CGKeyCode(kVK_ANSI_V), + keyDown: false) + else { return false } + keyDown.flags = .maskCommand + keyUp.flags = .maskCommand + keyDown.postToPid(processIdentifier) + keyUp.postToPid(processIdentifier) + return true + } +} diff --git a/apps/macos/Sources/OpenClaw/QuickChatReplyBinding.swift b/apps/macos/Sources/OpenClaw/QuickChatReplyBinding.swift index e89698f065c..ec06b5514ff 100644 --- a/apps/macos/Sources/OpenClaw/QuickChatReplyBinding.swift +++ b/apps/macos/Sources/OpenClaw/QuickChatReplyBinding.swift @@ -8,6 +8,8 @@ final class QuickChatReplyBinding { private(set) var route: QuickChatRoutingTarget? private(set) var viewModel: OpenClawChatViewModel? + private(set) var isPastingReply = false + private(set) var pasteStatusMessage: String? @ObservationIgnored private var preparedRoute: QuickChatRoutingTarget? @ObservationIgnored private let viewModelFactory: ViewModelFactory @@ -46,6 +48,20 @@ final class QuickChatReplyBinding { self.route = nil self.preparedRoute = nil self.viewModel = nil + self.isPastingReply = false + self.pasteStatusMessage = nil + } + + func beginPaste() -> Bool { + guard !self.isPastingReply else { return false } + self.isPastingReply = true + self.pasteStatusMessage = nil + return true + } + + func finishPaste(message: String? = nil) { + self.isPastingReply = false + self.pasteStatusMessage = message } private static func makeViewModel(route: QuickChatRoutingTarget) -> OpenClawChatViewModel { diff --git a/apps/macos/Sources/OpenClaw/QuickChatTextView.swift b/apps/macos/Sources/OpenClaw/QuickChatTextView.swift index ba0d8120b41..863332b1c1e 100644 --- a/apps/macos/Sources/OpenClaw/QuickChatTextView.swift +++ b/apps/macos/Sources/OpenClaw/QuickChatTextView.swift @@ -3,8 +3,10 @@ import SwiftUI struct QuickChatTextView: NSViewRepresentable { @Binding var text: String + let selectionRange: NSRange? let onSubmit: (Bool) -> Void let onEscape: () -> Void + let onUserEdit: () -> Void let onHeightChange: (CGFloat) -> Void let onTextViewReady: (NSTextView) -> Void @@ -61,10 +63,27 @@ struct QuickChatTextView: NSViewRepresentable { guard let textView = scrollView.documentView as? QuickChatNSTextView else { return } textView.onSubmit = self.onSubmit textView.onEscape = self.onEscape - if textView.string != self.text, !textView.hasMarkedText() { + let textChanged = textView.string != self.text && !textView.hasMarkedText() + if textChanged { context.coordinator.isProgrammaticUpdate = true textView.string = self.text - textView.setSelectedRange(NSRange(location: textView.string.utf16.count, length: 0)) + } + if !textView.hasMarkedText(), let selectionRange = self.selectionRange { + let selection = NSRange( + location: min(selectionRange.location, textView.string.utf16.count), + length: 0) + if context.coordinator.lastAppliedSelectionRange != selection { + textView.setSelectedRange(selection) + context.coordinator.lastAppliedSelectionRange = selection + } + } else if textChanged { + let selection = NSRange(location: textView.string.utf16.count, length: 0) + textView.setSelectedRange(selection) + context.coordinator.lastAppliedSelectionRange = nil + } else if self.selectionRange == nil { + context.coordinator.lastAppliedSelectionRange = nil + } + if textChanged { context.coordinator.isProgrammaticUpdate = false } DispatchQueue.main.async { @@ -77,6 +96,7 @@ struct QuickChatTextView: NSViewRepresentable { var parent: QuickChatTextView weak var scrollView: NSScrollView? var isProgrammaticUpdate = false + var lastAppliedSelectionRange: NSRange? private var lastHeight: CGFloat = 0 init(_ parent: QuickChatTextView) { @@ -86,6 +106,7 @@ struct QuickChatTextView: NSViewRepresentable { func textDidChange(_ notification: Notification) { guard let textView = notification.object as? QuickChatNSTextView else { return } if !self.isProgrammaticUpdate { + self.parent.onUserEdit() self.parent.text = textView.string } self.updateHeight(for: textView) diff --git a/apps/macos/Sources/OpenClaw/QuickChatView.swift b/apps/macos/Sources/OpenClaw/QuickChatView.swift index 154c3cdf975..b50c0095402 100644 --- a/apps/macos/Sources/OpenClaw/QuickChatView.swift +++ b/apps/macos/Sources/OpenClaw/QuickChatView.swift @@ -10,14 +10,19 @@ struct QuickChatView: View { let onDismiss: () -> Void let onSendAccepted: (Bool) -> Void let onShowAgentPicker: () -> Void + let onShowModelMenu: () -> Void let onShowRecentSessions: () -> Void + let onToggleDictation: () -> Void + let onStopDictation: () -> Void let onCaptureTextContext: () -> Void let onShowCaptureMenu: () -> Void let onGrantPermissions: () -> Void + let onPasteReply: () -> Void let onContentHeightChange: (CGFloat) -> Void let onTextViewReady: (NSTextView) -> Void @State private var editorHeight: CGFloat = 30 + @Environment(\.accessibilityReduceMotion) private var reduceMotion var body: some View { ZStack { @@ -81,6 +86,7 @@ struct QuickChatView: View { private var inputRow: some View { HStack(spacing: 10) { self.agentChip + self.modelControl ZStack(alignment: .leading) { if self.model.text.isEmpty { @@ -95,8 +101,16 @@ struct QuickChatView: View { } QuickChatTextView( text: self.$model.text, + selectionRange: self.model.dictationSelectionRange, onSubmit: self.submit, - onEscape: self.onDismiss, + onEscape: { + self.onStopDictation() + self.onDismiss() + }, + onUserEdit: { + guard self.model.isDictating || self.model.isStartingDictation else { return } + self.onStopDictation() + }, onHeightChange: { self.editorHeight = $0 }, onTextViewReady: self.onTextViewReady) .frame(height: self.editorHeight) @@ -115,6 +129,27 @@ struct QuickChatView: View { .help("Continue a recent conversation") .accessibilityLabel("Continue a recent conversation") + Button(action: self.onToggleDictation) { + Group { + if self.model.isStartingDictation { + ProgressView() + .controlSize(.small) + } else { + Image(systemName: self.model.isDictating ? "mic.fill" : "mic") + .font(.system(size: 16.5, weight: .medium)) + .foregroundStyle(self.model.isDictating ? Color.red : Color.secondary) + .symbolEffect( + .pulse, + isActive: self.model.isDictating && !self.reduceMotion) + } + } + .frame(width: 22, height: 22) + } + .buttonStyle(.plain) + .disabled(!self.model.canToggleDictation) + .help(self.dictationButtonLabel) + .accessibilityLabel(Text(verbatim: self.dictationButtonLabel)) + Button(action: self.onCaptureTextContext) { Group { if self.model.isCapturingTextContext { @@ -187,6 +222,32 @@ struct QuickChatView: View { } } + private var modelControl: some View { + Button(action: self.onShowModelMenu) { + HStack(spacing: 4) { + if self.model.isLoadingModelControls || self.model.isUpdatingModel { + ProgressView() + .controlSize(.mini) + } + Text(verbatim: self.model.modelControlLabel) + .lineLimit(1) + Image(systemName: "chevron.down") + .font(.system(size: 8, weight: .semibold)) + } + .font(.caption) + .foregroundStyle(.secondary) + .padding(.horizontal, 7) + .padding(.vertical, 4) + .background(Color.primary.opacity(0.06), in: Capsule()) + .frame(maxWidth: 125) + } + .buttonStyle(.plain) + .fixedSize() + .disabled(!self.model.canUseModelControls) + .help("Choose model and reasoning") + .accessibilityLabel("Model and reasoning") + } + private var agentAvatar: some View { ZStack { Circle() @@ -285,6 +346,30 @@ struct QuickChatView: View { isAttachmentInputEnabled: false) .id(self.replyBinding.route) .frame(height: 300) + if QuickChatPasteLogic.finalAssistantText( + messages: viewModel.messages, + afterUserIdempotencyKey: self.model.lastAcceptedIdempotencyKey, + streamingAssistantText: viewModel.streamingAssistantText, + pendingRunCount: viewModel.pendingRunCount) != nil + { + HStack { + Spacer() + Button { + self.onPasteReply() + } label: { + if self.replyBinding.isPastingReply { + ProgressView() + .controlSize(.small) + } else { + Label("Paste to \(self.model.frontmostAppName)", systemImage: "doc.on.clipboard") + } + } + .buttonStyle(.borderless) + .disabled(self.replyBinding.isPastingReply) + } + .padding(.horizontal, 12) + .padding(.bottom, 9) + } } } @@ -295,6 +380,15 @@ struct QuickChatView: View { if let message = self.model.textContextCaptureMessage { return (message, false) } + if let message = self.model.dictationStatusMessage { + return (message, true) + } + if let message = self.model.modelControlStatusMessage { + return (message, true) + } + if let message = self.replyBinding.pasteStatusMessage { + return (message, true) + } if let message = self.model.connectionStatusMessage { return (message, false) } @@ -302,6 +396,7 @@ struct QuickChatView: View { } private func submit(openChat: Bool) { + self.onStopDictation() guard self.model.canSend, let presentationID = self.model.activePresentationID else { return } Task { guard await self.model.send() else { return } @@ -316,4 +411,10 @@ struct QuickChatView: View { self.onSendAccepted(false) } } + + private var dictationButtonLabel: String { + self.model.isDictating || self.model.isStartingDictation + ? String(localized: "Stop dictation") + : String(localized: "Start dictation") + } } diff --git a/apps/macos/Tests/OpenClawIPCTests/QuickChatControllerTests.swift b/apps/macos/Tests/OpenClawIPCTests/QuickChatControllerTests.swift index ab67c51ac75..cf1319b9b5e 100644 --- a/apps/macos/Tests/OpenClawIPCTests/QuickChatControllerTests.swift +++ b/apps/macos/Tests/OpenClawIPCTests/QuickChatControllerTests.swift @@ -68,10 +68,12 @@ struct QuickChatControllerTests { ]) }, agentIdentityProvider: { _ in .placeholder }, - sendProvider: { _, _, _, _, _ in "ok" }, + sendProvider: { _, _, _, _, _, _ in "ok" }, permissionStatusProvider: { _ in [:] }, permissionGrantProvider: { _ in [:] }, - connectionGateProvider: { .available }) + connectionGateProvider: { .available }, + modelControlsProvider: { _ in .testFixture }, + modelPatchProvider: { _, _ in nil }) let controller = QuickChatController( enableUI: false, model: model, @@ -121,7 +123,7 @@ struct QuickChatControllerTests { agents: [AgentSummary(id: "main", name: "Main")]) }, agentIdentityProvider: { _ in .placeholder }, - sendProvider: { _, _, _, _, _ in "ok" }, + sendProvider: { _, _, _, _, _, _ in "ok" }, permissionStatusProvider: { capabilities in Dictionary(uniqueKeysWithValues: capabilities.map { ($0, $0 != .notifications) }) }, @@ -129,7 +131,9 @@ struct QuickChatControllerTests { await latch.wait() return Dictionary(uniqueKeysWithValues: capabilities.map { ($0, true) }) }, - connectionGateProvider: { .available }) + connectionGateProvider: { .available }, + modelControlsProvider: { _ in .testFixture }, + modelPatchProvider: { _, _ in nil }) let controller = QuickChatController(enableUI: false, model: model, monitoringEnabled: false) controller.present() guard let id = model.activePresentationID else { @@ -165,7 +169,7 @@ struct QuickChatControllerTests { agents: [AgentSummary(id: "main", name: "Main")]) }, agentIdentityProvider: { _ in .placeholder }, - sendProvider: { _, _, _, _, _ in "ok" }, + sendProvider: { _, _, _, _, _, _ in "ok" }, permissionStatusProvider: { capabilities in Dictionary(uniqueKeysWithValues: capabilities.map { ($0, true) }) }, @@ -173,7 +177,9 @@ struct QuickChatControllerTests { Dictionary(uniqueKeysWithValues: capabilities.map { ($0, true) }) }, connectionGateProvider: { .available }, - textContextCaptureProvider: { await latch.wait() }) + textContextCaptureProvider: { await latch.wait() }, + modelControlsProvider: { _ in .testFixture }, + modelPatchProvider: { _, _ in nil }) let controller = QuickChatController(enableUI: false, model: model, monitoringEnabled: false) controller.present() guard let id = model.activePresentationID else { @@ -216,10 +222,12 @@ struct QuickChatControllerTests { agents: [AgentSummary(id: "main", name: "Main")]) }, agentIdentityProvider: { _ in .placeholder }, - sendProvider: { _, _, _, _, _ in "ok" }, + sendProvider: { _, _, _, _, _, _ in "ok" }, permissionStatusProvider: { _ in [:] }, permissionGrantProvider: { _ in [:] }, - connectionGateProvider: { .available }) + connectionGateProvider: { .available }, + modelControlsProvider: { _ in .testFixture }, + modelPatchProvider: { _, _ in nil }) } } diff --git a/apps/macos/Tests/OpenClawIPCTests/QuickChatModelTests.swift b/apps/macos/Tests/OpenClawIPCTests/QuickChatModelTests.swift index 233f7d1eb45..897913bfe57 100644 --- a/apps/macos/Tests/OpenClawIPCTests/QuickChatModelTests.swift +++ b/apps/macos/Tests/OpenClawIPCTests/QuickChatModelTests.swift @@ -58,7 +58,7 @@ struct QuickChatModelTests { @Test func `unchanged draft reuses idempotency key after transport failure`() async { var keys: [String] = [] - let model = self.makeModel(sendHandler: { _, _, _, idempotencyKey, _ in + let model = self.makeModel(sendHandler: { _, _, _, _, idempotencyKey, _ in keys.append(idempotencyKey) if keys.count == 1 { throw FakeSendError.rejected } return "started" @@ -72,9 +72,44 @@ struct QuickChatModelTests { #expect(keys[0] == keys[1]) } + @Test func `no-op reasoning selection preserves idempotent retry`() async { + var keys: [String] = [] + let model = self.makeModel(sendHandler: { _, _, _, _, idempotencyKey, _ in + keys.append(idempotencyKey) + if keys.count == 1 { throw FakeSendError.rejected } + return "started" + }) + await self.prepare(model) + model.text = "hello" + + #expect(await !(model.send())) + model.selectThinkingLevel(nil) + #expect(await model.send()) + + #expect(keys.count == 2) + #expect(keys[0] == keys[1]) + } + + @Test func `new dispatch clears the previous accepted reply key`() async { + var shouldFail = false + let model = self.makeModel(sendHandler: { _, _, _, _, _, _ in + if shouldFail { throw FakeSendError.rejected } + return "started" + }) + await self.prepare(model) + model.text = "first" + #expect(await model.send()) + #expect(model.lastAcceptedIdempotencyKey != nil) + + shouldFail = true + model.text = "second" + #expect(await !(model.send())) + #expect(model.lastAcceptedIdempotencyKey == nil) + } + @Test func `edited draft gets new idempotency key`() async { var keys: [String] = [] - let model = self.makeModel(sendHandler: { _, _, _, idempotencyKey, _ in + let model = self.makeModel(sendHandler: { _, _, _, _, idempotencyKey, _ in keys.append(idempotencyKey) throw FakeSendError.rejected }) @@ -91,7 +126,7 @@ struct QuickChatModelTests { @Test func `empty text does not call gateway`() async { var sendCount = 0 - let model = self.makeModel(sendHandler: { _, _, _, _, _ in + let model = self.makeModel(sendHandler: { _, _, _, _, _, _ in sendCount += 1 return "ok" }) @@ -107,7 +142,7 @@ struct QuickChatModelTests { var sendCount = 0 let model = self.makeModel( gate: .disconnected, - sendHandler: { _, _, _, _, _ in + sendHandler: { _, _, _, _, _, _ in sendCount += 1 return "ok" }) @@ -135,7 +170,7 @@ struct QuickChatModelTests { @Test func `dismissal lets dispatched send settle without retry`() async { let latch = SendLatch() - let model = self.makeModel(sendHandler: { _, _, _, _, _ in + let model = self.makeModel(sendHandler: { _, _, _, _, _, _ in try await latch.wait() }) await self.prepare(model) @@ -188,7 +223,7 @@ struct QuickChatModelTests { sessionKeyProvider: { "main" }, agentsProvider: { Self.agentsResult(defaultID: "main", agentIDs: ["main"]) }, agentIdentityProvider: { _ in .placeholder }, - sendProvider: { _, _, _, _, _ in "ok" }, + sendProvider: { _, _, _, _, _, _ in "ok" }, permissionStatusProvider: { capabilities in Dictionary(uniqueKeysWithValues: capabilities.map { ($0, granted.value || $0 != .screenRecording) @@ -198,7 +233,9 @@ struct QuickChatModelTests { granted.value = true return Dictionary(uniqueKeysWithValues: capabilities.map { ($0, true) }) }, - connectionGateProvider: { .available }) + connectionGateProvider: { .available }, + modelControlsProvider: { _ in .testFixture }, + modelPatchProvider: { _, _ in nil }) await self.prepare(model) #expect(model.missingPermissions == [.screenRecording]) @@ -217,7 +254,7 @@ struct QuickChatModelTests { sessionKeyProvider: { "main" }, agentsProvider: { Self.agentsResult(defaultID: "main", agentIDs: ["main"]) }, agentIdentityProvider: { _ in .placeholder }, - sendProvider: { _, _, _, _, _ in "ok" }, + sendProvider: { _, _, _, _, _, _ in "ok" }, permissionStatusProvider: { capabilities in Dictionary(uniqueKeysWithValues: capabilities.map { ($0, $0 != .accessibility) }) }, @@ -225,7 +262,9 @@ struct QuickChatModelTests { await latch.wait() return Dictionary(uniqueKeysWithValues: capabilities.map { ($0, true) }) }, - connectionGateProvider: { .available }) + connectionGateProvider: { .available }, + modelControlsProvider: { _ in .testFixture }, + modelPatchProvider: { _, _ in nil }) await self.prepare(model) model.grantMissingPermissions() @@ -310,7 +349,7 @@ struct QuickChatModelTests { @Test func `override send uses canonical session key verbatim`() async { var sentRoute: QuickChatRoutingTarget? - let model = self.makeModel(sendHandler: { sessionKey, agentID, _, _, _ in + let model = self.makeModel(sendHandler: { sessionKey, agentID, _, _, _, _ in sentRoute = QuickChatRoutingTarget(sessionKey: sessionKey, agentID: agentID) return "started" }) @@ -332,7 +371,7 @@ struct QuickChatModelTests { agentIDs: ["main", "work"], scope: "global") }, - sendHandler: { sessionKey, agentID, _, _, _ in + sendHandler: { sessionKey, agentID, _, _, _, _ in sentRoute = QuickChatRoutingTarget(sessionKey: sessionKey, agentID: agentID) return "started" }) @@ -409,7 +448,7 @@ struct QuickChatModelTests { @Test func `edits during a screenshot send survive and keep their draft`() async throws { let latch = SendLatch() var receivedMessage: String? - let model = self.makeModel(sendHandler: { _, _, message, _, _ in + let model = self.makeModel(sendHandler: { _, _, message, _, _, _ in receivedMessage = message return try await latch.wait() }) @@ -439,7 +478,7 @@ struct QuickChatModelTests { @Test func `capture pipeline blocks concurrent sends and unwinds`() async { var sendCount = 0 - let model = self.makeModel(sendHandler: { _, _, _, _, _ in + let model = self.makeModel(sendHandler: { _, _, _, _, _, _ in sendCount += 1 return "ok" }) @@ -478,7 +517,7 @@ struct QuickChatModelTests { @Test func `window screenshot sends attachment and default caption`() async throws { var receivedMessage: String? var receivedAttachments: [OpenClawChatAttachmentPayload] = [] - let model = self.makeModel(sendHandler: { _, _, message, _, attachments in + let model = self.makeModel(sendHandler: { _, _, message, _, _, attachments in receivedMessage = message receivedAttachments = attachments return "started" @@ -523,7 +562,7 @@ struct QuickChatModelTests { @Test func `accepted send clears attached context`() async { var receivedMessage: String? - let model = self.makeModel(sendHandler: { _, _, message, _, _ in + let model = self.makeModel(sendHandler: { _, _, message, _, _, _ in receivedMessage = message return "ok" }) @@ -591,7 +630,7 @@ struct QuickChatModelTests { agentIdentityProvider: { _ in QuickChatAgentDisplay(id: "main", name: "Molty", emoji: "🦞") }, - sendProvider: sendHandler ?? { _, _, _, _, _ in + sendProvider: sendHandler ?? { _, _, _, _, _, _ in if let sendError { throw sendError } return sendStatus }, @@ -601,7 +640,9 @@ struct QuickChatModelTests { permissionGrantProvider: { capabilities in Dictionary(uniqueKeysWithValues: capabilities.map { ($0, true) }) }, - connectionGateProvider: { gate }) + connectionGateProvider: { gate }, + modelControlsProvider: { _ in .testFixture }, + modelPatchProvider: { _, _ in nil }) } private static func agentsResult( diff --git a/apps/macos/Tests/OpenClawIPCTests/QuickChatPowerFeaturesTests.swift b/apps/macos/Tests/OpenClawIPCTests/QuickChatPowerFeaturesTests.swift new file mode 100644 index 00000000000..7a0ca96b7be --- /dev/null +++ b/apps/macos/Tests/OpenClawIPCTests/QuickChatPowerFeaturesTests.swift @@ -0,0 +1,470 @@ +import Foundation +import OpenClawChatUI +import OpenClawProtocol +import Testing +@testable import OpenClaw + +extension QuickChatModelControlSnapshot { + static let testFixture = QuickChatModelControlSnapshot( + models: [], + currentModelSelectionID: nil, + currentThinkingLevel: nil, + thinkingOptions: QuickChatModelControlLogic.baseThinkingOptions, + defaultProvider: nil) +} + +@MainActor +struct QuickChatPowerFeaturesTests { + @Test func `dictation inserts at a UTF16 caret and replaces each partial`() { + let text = "Hello" + let session = QuickChatDictationTextSession( + baseText: text, + replacementRange: NSRange(location: text.utf16.count, length: 0)) + + #expect(session.update(transcript: "swift").text == "Hello swift") + let final = session.update(transcript: "swift world") + #expect(final.text == "Hello swift world") + #expect(final.selection == NSRange(location: final.text.utf16.count, length: 0)) + } + + @Test func `dictation replaces the selected composer span`() { + let text = "Use old model" + let range = (text as NSString).range(of: "old") + let update = QuickChatDictationTextSession( + baseText: text, + replacementRange: range) + .update(transcript: "new") + + #expect(update.text == "Use new model") + #expect(update.selection == NSRange(location: range.location + 3, length: 0)) + } + + @Test func `dictation caret respects extended grapheme UTF16 offsets`() { + let prefix = "Hi 🦞" + let text = "\(prefix)!" + let update = QuickChatDictationTextSession( + baseText: text, + replacementRange: NSRange(location: prefix.utf16.count, length: 0)) + .update(transcript: "there") + + #expect(update.text == "Hi 🦞 there!") + } + + @Test func `paste extracts only a completed assistant reply after the accepted send`() { + let messages = [ + Self.message(role: "user", text: "Question", idempotencyKey: "send-1"), + Self.message(role: "assistant", text: "hidden\nVisible **answer**"), + ] + + #expect(QuickChatPasteLogic.finalAssistantText( + messages: messages, + afterUserIdempotencyKey: "send-1", + streamingAssistantText: nil, + pendingRunCount: 0) == "Visible **answer**") + #expect(QuickChatPasteLogic.finalAssistantText( + messages: messages, + afterUserIdempotencyKey: "send-1", + streamingAssistantText: "Visible", + pendingRunCount: 0) == nil) + #expect(QuickChatPasteLogic.finalAssistantText( + messages: messages, + afterUserIdempotencyKey: "send-1", + streamingAssistantText: nil, + pendingRunCount: 1) == nil) + } + + @Test func `paste rejects a stale assistant and the OpenClaw process`() { + let messages = [ + Self.message(role: "user", text: "Question", idempotencyKey: "send-1"), + Self.message(role: "assistant", text: "Answer"), + Self.message(role: "user", text: "Follow-up", idempotencyKey: "send-2"), + Self.message(role: "assistant", text: "Different answer"), + ] + #expect(QuickChatPasteLogic.finalAssistantText( + messages: messages, + afterUserIdempotencyKey: "send-1", + streamingAssistantText: nil, + pendingRunCount: 0) == nil) + #expect(!QuickChatPasteLogic.canPaste(frontmostProcessIdentifier: 42, ownProcessIdentifier: 42)) + #expect(QuickChatPasteLogic.canPaste(frontmostProcessIdentifier: 43, ownProcessIdentifier: 42)) + #expect(!QuickChatPasteLogic.canPaste(frontmostProcessIdentifier: nil, ownProcessIdentifier: 42)) + #expect(QuickChatPasteLogic.isExpectedTarget( + frontmostProcessIdentifier: 43, + targetProcessIdentifier: 43)) + #expect(!QuickChatPasteLogic.isExpectedTarget( + frontmostProcessIdentifier: 44, + targetProcessIdentifier: 43)) + } + + @Test func `models list fixture builds current state and provider menu sections`() throws { + let models = [ + OpenClawChatModelChoice( + modelID: "claude-sonnet-4-6", + name: "Sonnet", + provider: "anthropic", + contextWindow: 200_000, + reasoning: true), + OpenClawChatModelChoice( + modelID: "gpt-5.6-sol", + name: "Sol", + provider: "openai", + contextWindow: 400_000, + reasoning: true), + ] + let sessions = try JSONDecoder().decode( + OpenClawChatSessionsListResponse.self, + from: Data(Self.sessionsFixture.utf8)) + let agents = try JSONDecoder().decode( + AgentsListResult.self, + from: Data(Self.agentsFixture.utf8)) + let snapshot = QuickChatModelControlLogic.snapshot( + target: QuickChatRoutingTarget(sessionKey: "agent:main:main", agentID: nil), + models: models, + sessions: sessions, + agents: agents) + let sections = ChatModelPickerStore.sections( + choices: snapshot.models, + favorites: [], + recents: [], + defaultProvider: snapshot.defaultProvider) + + #expect(snapshot.currentModelSelectionID == "openai/gpt-5.6-sol") + #expect(snapshot.currentThinkingLevel == "medium") + #expect(snapshot.thinkingOptions.map(\.id) == ["off", "medium", "high"]) + #expect(sections.providers.map(\.id) == ["openai", "anthropic"]) + #expect(sections.providers.first?.isDefaultProvider == true) + } + + @Test func `model controls use target agent defaults when no session row exists`() throws { + let sessions = try JSONDecoder().decode( + OpenClawChatSessionsListResponse.self, + from: Data(Self.sessionsFixture.utf8)) + let agents = try JSONDecoder().decode( + AgentsListResult.self, + from: Data(Self.agentsFixture.utf8)) + let snapshot = QuickChatModelControlLogic.snapshot( + target: QuickChatRoutingTarget(sessionKey: "agent:work:main", agentID: nil), + models: [], + sessions: sessions, + agents: agents) + + #expect(snapshot.currentModelSelectionID == "deepseek/deepseek-v4") + #expect(snapshot.currentThinkingLevel == "high") + #expect(snapshot.thinkingOptions.map(\.id) == ["off", "high"]) + #expect(snapshot.defaultProvider == "deepseek") + } + + @Test func `model patch decision only patches an explicit unapplied selection`() { + #expect(QuickChatModelControlLogic.modelPatchDecision( + selectionID: nil, + appliedSelectionID: nil) == .none) + #expect(QuickChatModelControlLogic.modelPatchDecision( + selectionID: "openai/gpt-5.6-sol", + appliedSelectionID: "openai/gpt-5.6-sol") == .none) + #expect(QuickChatModelControlLogic.modelPatchDecision( + selectionID: "openai/gpt-5.6-sol", + appliedSelectionID: "openai/gpt-5.6-sol", + currentSessionSelectionID: "anthropic/claude-sonnet-4-6") == .patch("openai/gpt-5.6-sol")) + #expect(QuickChatModelControlLogic.modelPatchDecision( + selectionID: "openai/gpt-5.6-sol", + appliedSelectionID: nil, + currentSessionSelectionID: "openai/gpt-5.6-sol") == .none) + #expect(QuickChatModelControlLogic.modelPatchDecision( + selectionID: "openai/gpt-5.6-sol", + appliedSelectionID: nil) == .patch("openai/gpt-5.6-sol")) + #expect(QuickChatModelControlLogic.modelPatchDecision( + selectionID: OpenClawChatViewModel.defaultModelSelectionID, + appliedSelectionID: nil) == .patch(nil)) + } + + @Test func `model thinking options reject an unsupported explicit override`() { + let options = [OpenClawChatThinkingLevelOption(id: "off", label: "Off")] + + #expect(QuickChatModelControlLogic.validatedThinkingSelection("off", options: options) == "off") + #expect(QuickChatModelControlLogic.validatedThinkingSelection("high", options: options) == nil) + #expect(QuickChatModelControlLogic.validatedThinkingSelection(nil, options: options) == nil) + } + + @Test func `reasoning override threads into chat send per message`() async throws { + var sentThinking: String? + let model = QuickChatModel( + sessionKeyProvider: { "main" }, + agentsProvider: { + AgentsListResult( + defaultid: "main", + mainkey: "main", + scope: AnyCodable("per-agent"), + agents: [AgentSummary(id: "main", name: "Main")]) + }, + agentIdentityProvider: { _ in .placeholder }, + sendProvider: { _, _, _, thinking, _, _ in + sentThinking = thinking + return "ok" + }, + permissionStatusProvider: { capabilities in + Dictionary(uniqueKeysWithValues: capabilities.map { ($0, true) }) + }, + permissionGrantProvider: { capabilities in + Dictionary(uniqueKeysWithValues: capabilities.map { ($0, true) }) + }, + connectionGateProvider: { .available }, + modelControlsProvider: { _ in + QuickChatModelControlSnapshot( + models: [], + currentModelSelectionID: nil, + currentThinkingLevel: "low", + thinkingOptions: QuickChatModelControlLogic.baseThinkingOptions, + defaultProvider: nil) + }, + modelPatchProvider: { _, _ in nil }) + let presentationID = model.beginPresentation() + await model.refreshForPresentation(id: presentationID) + try await Self.waitForModelControls(model) + model.selectThinkingLevel("high") + model.text = "Hello" + + #expect(await model.send()) + #expect(sentThinking == "high") + } + + @Test func `session model patch settles and send aborts after dismissal`() async throws { + let latch = QuickChatModelPatchLatch() + var sendCount = 0 + let choice = OpenClawChatModelChoice( + modelID: "gpt-5.6-sol", + name: "Sol", + provider: "openai", + contextWindow: 400_000, + reasoning: true) + let model = Self.model( + sendProvider: { _, _, _, _, _, _ in + sendCount += 1 + return "ok" + }, + controlsProvider: { _ in + QuickChatModelControlSnapshot( + models: [choice], + currentModelSelectionID: nil, + currentThinkingLevel: nil, + thinkingOptions: QuickChatModelControlLogic.baseThinkingOptions, + defaultProvider: nil) + }, + patchProvider: { _, _ in try await latch.wait() }) + let presentationID = model.beginPresentation() + await model.refreshForPresentation(id: presentationID) + try await Self.waitForModelControls(model) + + model.selectModel(choice.selectionID) + model.text = "Hello" + let send = Task { await model.send() } + model.endPresentation() + try await latch.waitUntilStarted() + latch.finish(with: OpenClawChatModelPatchResult( + modelProvider: choice.provider, + model: choice.modelID, + thinkingLevel: nil, + thinkingLevels: QuickChatModelControlLogic.baseThinkingOptions)) + try await Self.waitForModelPatch(model) + + #expect(latch.completed) + #expect(!(await send.value)) + #expect(sendCount == 0) + } + + @Test func `failed post-patch refresh preserves and blocks explicit reasoning`() async throws { + let choice = OpenClawChatModelChoice( + modelID: "gpt-5.6-sol", + name: "Sol", + provider: "openai", + contextWindow: 400_000, + reasoning: true) + var controlsCallCount = 0 + let model = Self.model( + controlsProvider: { _ in + controlsCallCount += 1 + if controlsCallCount > 1 { throw QuickChatModelControlsTestError.refreshFailed } + return QuickChatModelControlSnapshot( + models: [choice], + currentModelSelectionID: nil, + currentThinkingLevel: nil, + thinkingOptions: QuickChatModelControlLogic.baseThinkingOptions, + defaultProvider: nil) + }, + patchProvider: { _, _ in + OpenClawChatModelPatchResult( + modelProvider: choice.provider, + model: choice.modelID, + thinkingLevel: nil) + }) + let presentationID = model.beginPresentation() + await model.refreshForPresentation(id: presentationID) + try await Self.waitForModelControls(model) + model.selectThinkingLevel("high") + model.selectModel(choice.selectionID) + let refreshClock = ContinuousClock() + let refreshDeadline = refreshClock.now.advanced(by: .seconds(1)) + while controlsCallCount < 2 { + guard refreshClock.now < refreshDeadline else { + throw QuickChatModelControlsTestError.timeout + } + await Task.yield() + } + try await Self.waitForModelPatch(model) + model.text = "Hello" + + #expect(model.selectedThinkingLevel == "high") + #expect(model.thinkingOptions.isEmpty) + #expect(!model.isSelectedThinkingLevelSupported) + #expect(!model.canSend) + } + + private static func model( + sendProvider: @escaping QuickChatModel.SendProvider = { _, _, _, _, _, _ in "ok" }, + controlsProvider: @escaping QuickChatModel.ModelControlsProvider, + patchProvider: @escaping QuickChatModel.ModelPatchProvider) -> QuickChatModel + { + QuickChatModel( + sessionKeyProvider: { "agent:main:main" }, + agentsProvider: { + AgentsListResult( + defaultid: "main", + mainkey: "main", + scope: AnyCodable("per-agent"), + agents: [AgentSummary(id: "main", name: "Main")]) + }, + agentIdentityProvider: { _ in .placeholder }, + sendProvider: sendProvider, + permissionStatusProvider: { capabilities in + Dictionary(uniqueKeysWithValues: capabilities.map { ($0, true) }) + }, + permissionGrantProvider: { capabilities in + Dictionary(uniqueKeysWithValues: capabilities.map { ($0, true) }) + }, + connectionGateProvider: { .available }, + modelControlsProvider: controlsProvider, + modelPatchProvider: patchProvider) + } + + private static func waitForModelControls(_ model: QuickChatModel) async throws { + let clock = ContinuousClock() + let deadline = clock.now.advanced(by: .seconds(1)) + while model.isLoadingModelControls { + guard clock.now < deadline else { throw QuickChatModelControlsTestError.timeout } + await Task.yield() + } + } + + private static func waitForModelPatch(_ model: QuickChatModel) async throws { + let clock = ContinuousClock() + let deadline = clock.now.advanced(by: .seconds(1)) + while model.isUpdatingModel { + guard clock.now < deadline else { throw QuickChatModelControlsTestError.timeout } + await Task.yield() + } + } + + private static func message( + role: String, + text: String, + idempotencyKey: String? = nil) -> OpenClawChatMessage + { + OpenClawChatMessage( + role: role, + content: [OpenClawChatMessageContent( + type: "text", + text: text, + mimeType: nil, + fileName: nil, + content: nil)], + timestamp: 1, + idempotencyKey: idempotencyKey) + } + + private static let sessionsFixture = """ + { + "defaults": { + "modelProvider": "anthropic", + "model": "claude-sonnet-4-6", + "contextTokens": 200000, + "thinkingLevels": [ + {"id": "off", "label": "off"}, + {"id": "medium", "label": "medium"}, + {"id": "high", "label": "high"} + ], + "thinkingDefault": "low", + "mainSessionKey": "agent:main:main" + }, + "sessions": [ + { + "key": "agent:main:main", + "modelProvider": "openai", + "model": "gpt-5.6-sol", + "thinkingLevel": "medium" + } + ] + } + """ + + private static let agentsFixture = """ + { + "defaultId": "main", + "mainKey": "main", + "scope": "per-sender", + "agents": [ + { + "id": "main", + "model": {"primary": "anthropic/claude-sonnet-4-6"}, + "thinkingDefault": "low" + }, + { + "id": "work", + "model": {"primary": "deepseek/deepseek-v4"}, + "thinkingDefault": "high", + "thinkingLevels": [ + {"id": "off", "label": "Off"}, + {"id": "high", "label": "High"} + ] + } + ] + } + """ +} + +private enum QuickChatModelControlsTestError: Error { + case refreshFailed + case timeout +} + +@MainActor +private final class QuickChatModelPatchLatch { + private var continuation: CheckedContinuation? + private(set) var completed = false + + var started: Bool { + self.continuation != nil + } + + func wait() async throws -> OpenClawChatModelPatchResult { + try Task.checkCancellation() + let result = await withCheckedContinuation { continuation in + self.continuation = continuation + } + try Task.checkCancellation() + self.completed = true + return result + } + + func waitUntilStarted() async throws { + let clock = ContinuousClock() + let deadline = clock.now.advanced(by: .seconds(1)) + while !self.started { + guard clock.now < deadline else { throw QuickChatModelControlsTestError.timeout } + await Task.yield() + } + } + + func finish(with result: OpenClawChatModelPatchResult) { + self.continuation?.resume(returning: result) + self.continuation = nil + } +} diff --git a/apps/macos/Tests/OpenClawIPCTests/QuickChatViewSmokeTests.swift b/apps/macos/Tests/OpenClawIPCTests/QuickChatViewSmokeTests.swift index 2418ee63fa8..1ee1a24d7cf 100644 --- a/apps/macos/Tests/OpenClawIPCTests/QuickChatViewSmokeTests.swift +++ b/apps/macos/Tests/OpenClawIPCTests/QuickChatViewSmokeTests.swift @@ -17,24 +17,30 @@ struct QuickChatViewSmokeTests { agents: [AgentSummary(id: "main", name: "Agent")]) }, agentIdentityProvider: { _ in QuickChatAgentDisplay(id: "main", name: "Agent", emoji: nil) }, - sendProvider: { _, _, _, _, _ in "ok" }, + sendProvider: { _, _, _, _, _, _ in "ok" }, permissionStatusProvider: { capabilities in Dictionary(uniqueKeysWithValues: capabilities.map { ($0, true) }) }, permissionGrantProvider: { capabilities in Dictionary(uniqueKeysWithValues: capabilities.map { ($0, true) }) }, - connectionGateProvider: { .available }) + connectionGateProvider: { .available }, + modelControlsProvider: { _ in .testFixture }, + modelPatchProvider: { _, _ in nil }) let view = QuickChatView( model: model, replyBinding: QuickChatReplyBinding(), onDismiss: {}, onSendAccepted: { _ in }, onShowAgentPicker: {}, + onShowModelMenu: {}, onShowRecentSessions: {}, + onToggleDictation: {}, + onStopDictation: {}, onCaptureTextContext: {}, onShowCaptureMenu: {}, onGrantPermissions: {}, + onPasteReply: {}, onContentHeightChange: { _ in }, onTextViewReady: { _ in }) diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageVisibleText.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageVisibleText.swift index 4d15c425076..7e0c30f0926 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageVisibleText.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageVisibleText.swift @@ -4,13 +4,13 @@ import Foundation /// in the bubble, with tool traces and non-text blocks removed. Shared by the /// transcript exporter and the Listen action so exported and spoken text /// always match the visible transcript. -enum ChatMessageVisibleText { +public enum ChatMessageVisibleText { static func copyText(in message: OpenClawChatMessage) -> String { let role = message.role.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() return role == "assistant" ? self.visibleText(in: message) : self.primaryText(in: message) } - static func visibleText(in message: OpenClawChatMessage) -> String { + public static func visibleText(in message: OpenClawChatMessage) -> String { let text = self.primaryText(in: message) let role = message.role.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() guard role != "user" else { return text } diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatModelPickerStore.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatModelPickerStore.swift index a6a1bad7f95..ef4cfa2b755 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatModelPickerStore.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatModelPickerStore.swift @@ -53,7 +53,7 @@ public final class ChatModelPickerStore { self.defaults.set(self.recents, forKey: Self.recentsKey) } - static func sections( + public static func sections( choices: [OpenClawChatModelChoice], favorites: [String], recents: [String], diff --git a/docs/platforms/mac/webchat.md b/docs/platforms/mac/webchat.md index 7c14a98c80d..014ed6ec071 100644 --- a/docs/platforms/mac/webchat.md +++ b/docs/platforms/mac/webchat.md @@ -22,6 +22,10 @@ Press Option-Space (⌥Space) or choose **Quick Chat** from the menu bar menu to Quick Chat shows the targeted agent (avatar or emoji, with the agent's name as the placeholder) and sends to that agent's main session. After Return accepts a send, the bar stays open and expands downward with the streamed Markdown reply and recent transcript. The bar input remains the composer. Press Command-Return to send and open the same target in the full chat window, Shift-Return for a newline, or Escape to dismiss the whole bar and reply area. Clicking outside also dismisses it. When relevant macOS permissions are missing, an attached strip offers **Grant** and **Not now** actions. +Use the microphone button to dictate into the composer. Partial speech results replace the dictated span live while preserving text that was already in the composer. Press the button again, Return, or Escape to stop; sending, hiding, or unfocusing Quick Chat also releases the microphone. The first use asks for macOS Microphone and Speech Recognition access. + +The compact model control shows the target session's current model and reasoning level. A model choice updates that session and therefore persists there, while a reasoning choice applies only to each message sent from the current Quick Chat presentation. Local choices reset when the bar hides. Switching agents or choosing a recent session keeps explicit choices but reloads the newly targeted session's underlying model state. + Click the history button to choose from the five most recently updated sessions or return to **New message to <agent>**. A recent selection sends to that exact session and changes the placeholder to **Reply in <session>**. Hiding Quick Chat resets this temporary target to the selected agent's main session; switching agents from the avatar menu also clears it. Command-Return opens the conversation of the agent that received the send, including when session scope is global. @@ -30,6 +34,8 @@ The camera button opens a menu for **Capture Window…** or **Capture Area…**. Use the document-text button to attach text from the focused app's focused window. Quick Chat shows the result as a removable context chip rather than placing the captured text in the composer; sending appends the chip's text to the outgoing message and then clears it. This requires macOS Accessibility permission. Attached text also clears whenever Quick Chat closes, so context from one presentation cannot leak into a later send. +After a reply finishes, choose **Paste to <app>** to copy its visible assistant text, excluding hidden reasoning, to the general pasteboard and paste it into the app that was frontmost. This requires macOS Accessibility permission. The action replaces the current pasteboard contents and then hides Quick Chat. + Disable the feature entirely with **Settings → General → Quick Chat**; the same section hosts the shortcut recorder. - **Local mode**: connects directly to the local Gateway WebSocket.