feat(macos): Quick Chat power features — voice dictation, paste-to-app, model/reasoning control (#110994)

* 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
This commit is contained in:
Peter Steinberger
2026-07-18 17:21:03 -07:00
committed by GitHub
parent e5b2ca3c6b
commit 45f6543c9a
39 changed files with 4302 additions and 58 deletions
+185 -17
View File
@@ -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,
+105
View File
@@ -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",
+105
View File
@@ -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",
+105
View File
@@ -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",
+105
View File
@@ -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",
+105
View File
@@ -21118,6 +21118,41 @@
"source": "Continue",
"translated": "Continuer"
},
{
"id": "native.apple.237f02309f0b9aeb",
"source": "Microphone and speech recognition permission required.",
"translated": "Lautorisation daccéder au microphone et à la reconnaissance vocale est requise."
},
{
"id": "native.apple.6f2679049010dae7",
"source": "Dictation stopped because speech recognition failed.",
"translated": "La dictée sest 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 nest 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": "Lautorisation daccessibilité 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é nest 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",
+105
View File
@@ -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",
+105
View File
@@ -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",
+105
View File
@@ -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",
+105
View File
@@ -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",
+105
View File
@@ -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",
+105
View File
@@ -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",
+105
View File
@@ -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",
+105
View File
@@ -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",
+105
View File
@@ -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",
+105
View File
@@ -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",
+105
View File
@@ -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",
+105
View File
@@ -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",
+105
View File
@@ -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",
+105
View File
@@ -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",
+105
View File
@@ -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",
+105
View File
@@ -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",
@@ -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) })
},
@@ -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<QuickChatView>?
@@ -113,10 +114,15 @@ final class QuickChatController: NSObject, NSWindowDelegate {
@ObservationIgnored private var isMenuActive = false
@ObservationIgnored private var recentSessionsTask: Task<Void, Never>?
@ObservationIgnored private var recentSessionsRequestID = UUID()
@ObservationIgnored private var dictationStartTask: Task<Void, Never>?
@ObservationIgnored private var dictationRequestID = UUID()
@ObservationIgnored private var pasteTask: Task<Void, Never>?
@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()
}
@@ -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.Index>) -> String {
guard self.replacementRange.length == 0 else { return transcript }
let prefix = self.baseText[..<range.lowerBound]
let suffix = self.baseText[range.upperBound...]
var insertion = transcript
if let previous = prefix.last, !previous.isWhitespace,
let first = insertion.first, !first.isWhitespace
{
insertion.insert(" ", at: insertion.startIndex)
}
if let next = suffix.first, !next.isWhitespace, !Self.isPunctuation(next),
let last = insertion.last, !last.isWhitespace
{
insertion.append(" ")
}
return insertion
}
private static func isPunctuation(_ character: Character) -> 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
}
}
@@ -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<Void, Never>?
@ObservationIgnored private var dictationTextSession: QuickChatDictationTextSession?
@ObservationIgnored private var modelControlsTask: Task<Void, Never>?
@ObservationIgnored private var modelControlsRequestID = UUID()
@ObservationIgnored private var modelPatchTask: Task<OpenClawChatModelPatchResult?, Error>?
@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<OpenClawChatModelPatchResult?, Error>,
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)
@@ -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<String>()
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[..<separator])
}
private static func normalized(_ value: String?) -> String? {
let value = value?.trimmingCharacters(in: .whitespacesAndNewlines)
return value?.isEmpty == false ? value : nil
}
}
@@ -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)
}
}
}
@@ -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
}
}
@@ -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 {
@@ -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)
+102 -1
View File
@@ -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")
}
}
@@ -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 })
}
}
@@ -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(
@@ -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: "<think>hidden</think>\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<OpenClawChatModelPatchResult, Never>?
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
}
}
@@ -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 })
@@ -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 }
@@ -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],
+6
View File
@@ -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 &lt;agent&gt;**. A recent selection sends to that exact session and changes the placeholder to **Reply in &lt;session&gt;**. 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 &lt;app&gt;** 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.