mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 02:06:43 +00:00
feat(apps,ui): question surface parity and composer takeover (#110681)
* feat(apple): question parity — persistent summaries, skip, reconnect fallback, macOS keyboard * feat(android): question parity — persistent summaries, skip, reconnect fallback * feat(ui): question panel replaces composer while expanded * fix(apps): regenerate question parity contracts * fix(apps): preserve question reconciliation state * fix(apps): keep question terminal state monotonic * test(android): satisfy question test formatting * fix(security): restore skill audit file reads * fix(apps): harden question parity edge cases * fix(apps): preserve expired question summaries * fix(apps): keep question recovery retries current * fix(apps): retry stale question recovery snapshots * fix(android): reset stale question retry budgets * docs(tools): clarify question composer takeover * fix(apps): satisfy question recovery release gates * fix(apps): preserve unknown question outcomes * fix(apps): harden question action state * fix(apps): keep unavailable questions terminal * fix(apps): finalize local question outcomes * fix(apps): preserve question card API * fix(apps): reconcile external question answers * fix(apps): reset question backoff budget * chore(apps): refresh native i18n inventory * fix(android): distinguish question skip progress * fix(android): reconcile locally expired questions * fix(apps): keep question recovery state fresh * docs: note question surface parity * fix(ui): reconcile IME draft before question takeover * docs: leave question parity notes to release workflow
This commit is contained in:
@@ -9098,6 +9098,14 @@
|
||||
"Ta bort från arkiv"
|
||||
]
|
||||
},
|
||||
{
|
||||
"locale": "ja-JP",
|
||||
"source": "Unavailable",
|
||||
"translations": [
|
||||
"利用できません",
|
||||
"利用不可"
|
||||
]
|
||||
},
|
||||
{
|
||||
"locale": "sv",
|
||||
"source": "Unavailable",
|
||||
|
||||
+302
-254
File diff suppressed because it is too large
Load Diff
+71
-41
@@ -7388,11 +7388,36 @@
|
||||
"source": "Other answer",
|
||||
"translated": "إجابة أخرى"
|
||||
},
|
||||
{
|
||||
"id": "native.android.de591e6e2e440ecb",
|
||||
"source": "Skip",
|
||||
"translated": "تخطي"
|
||||
},
|
||||
{
|
||||
"id": "native.android.92dfa7979d04989c",
|
||||
"source": "Submitting…",
|
||||
"translated": "جارٍ الإرسال…"
|
||||
},
|
||||
{
|
||||
"id": "native.android.fa81739f52e7697f",
|
||||
"source": "Submit",
|
||||
"translated": "إرسال"
|
||||
},
|
||||
{
|
||||
"id": "native.android.7da074848cacfe4b",
|
||||
"source": "Skipped",
|
||||
"translated": "تم التخطي"
|
||||
},
|
||||
{
|
||||
"id": "native.android.1a0e6b33059a63aa",
|
||||
"source": "Expired",
|
||||
"translated": "انتهت الصلاحية"
|
||||
},
|
||||
{
|
||||
"id": "native.android.86c8d6113d07ef82",
|
||||
"source": "Unavailable",
|
||||
"translated": "غير متاح"
|
||||
},
|
||||
{
|
||||
"id": "native.android.f68cd7a84be4089e",
|
||||
"source": "Answered",
|
||||
@@ -7403,36 +7428,6 @@
|
||||
"source": "Answered elsewhere",
|
||||
"translated": "تمت الإجابة في مكان آخر"
|
||||
},
|
||||
{
|
||||
"id": "native.android.1a0e6b33059a63aa",
|
||||
"source": "Expired",
|
||||
"translated": "انتهت الصلاحية"
|
||||
},
|
||||
{
|
||||
"id": "native.android.d6d8fc83cec68418",
|
||||
"source": "Cancelled",
|
||||
"translated": "تم الإلغاء"
|
||||
},
|
||||
{
|
||||
"id": "native.android.73f64d0d54b7f9c0",
|
||||
"source": "Pending",
|
||||
"translated": "قيد الانتظار"
|
||||
},
|
||||
{
|
||||
"id": "native.android.92dfa7979d04989c",
|
||||
"source": "Submitting…",
|
||||
"translated": "جارٍ الإرسال…"
|
||||
},
|
||||
{
|
||||
"id": "native.android.c5e9ee99f9d9c612",
|
||||
"source": "Expires in ${seconds / 60}m ${seconds % 60}s",
|
||||
"translated": "تنتهي الصلاحية خلال ${seconds / 60}د ${seconds % 60}ث"
|
||||
},
|
||||
{
|
||||
"id": "native.android.2ca602fbdf96f1e7",
|
||||
"source": "Expires in ${seconds}s",
|
||||
"translated": "تنتهي الصلاحية خلال ${seconds}ث"
|
||||
},
|
||||
{
|
||||
"id": "native.android.118e8d239b18895e",
|
||||
"source": "Model",
|
||||
@@ -23038,6 +23033,41 @@
|
||||
"source": "Expand plan",
|
||||
"translated": "توسيع الخطة"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.ce899320a84d4086",
|
||||
"source": "Answered",
|
||||
"translated": "تمت الإجابة"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f019b6a07d5a33d8",
|
||||
"source": "Answered elsewhere",
|
||||
"translated": "تمت الإجابة في مكان آخر"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.ebdedeffad570c43",
|
||||
"source": "Skipped",
|
||||
"translated": "تم التخطي"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.8dde2eaaf6b0dfa9",
|
||||
"source": "Expired",
|
||||
"translated": "انتهت الصلاحية"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6dff9e9194347f3b",
|
||||
"source": "Unavailable",
|
||||
"translated": "غير متاح"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b34cd890366824f0",
|
||||
"source": "Pending",
|
||||
"translated": "قيد الانتظار"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b97da55b7e16e60c",
|
||||
"source": "Question summary",
|
||||
"translated": "ملخص السؤال"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.da4494f9e875169d",
|
||||
"source": "Other answer",
|
||||
@@ -23054,9 +23084,14 @@
|
||||
"translated": "محدد"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.e3095b651fc46b52",
|
||||
"source": "Submit",
|
||||
"translated": "إرسال"
|
||||
"id": "native.apple.1bbbbfd1ad3842ce",
|
||||
"source": "Skipping…",
|
||||
"translated": "جارٍ التخطي…"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.14a4bf7b6b7d40dc",
|
||||
"source": "Skip",
|
||||
"translated": "تخطي"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6c1adc16e0a846e5",
|
||||
@@ -23064,14 +23099,9 @@
|
||||
"translated": "جارٍ الإرسال…"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.698bdb0d6537edb6",
|
||||
"source": "Expires in \\(seconds / 60)m \\(seconds % 60)s",
|
||||
"translated": "تنتهي الصلاحية خلال \\(seconds / 60)د \\(seconds % 60)ث"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.30e1c342b3345290",
|
||||
"source": "Expires in \\(seconds)s",
|
||||
"translated": "تنتهي الصلاحية خلال \\(seconds)ث"
|
||||
"id": "native.apple.e3095b651fc46b52",
|
||||
"source": "Submit",
|
||||
"translated": "إرسال"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7cd6fb01b9f5d14a",
|
||||
|
||||
+71
-41
@@ -7388,11 +7388,36 @@
|
||||
"source": "Other answer",
|
||||
"translated": "Andere Antwort"
|
||||
},
|
||||
{
|
||||
"id": "native.android.de591e6e2e440ecb",
|
||||
"source": "Skip",
|
||||
"translated": "Überspringen"
|
||||
},
|
||||
{
|
||||
"id": "native.android.92dfa7979d04989c",
|
||||
"source": "Submitting…",
|
||||
"translated": "Wird gesendet…"
|
||||
},
|
||||
{
|
||||
"id": "native.android.fa81739f52e7697f",
|
||||
"source": "Submit",
|
||||
"translated": "Absenden"
|
||||
},
|
||||
{
|
||||
"id": "native.android.7da074848cacfe4b",
|
||||
"source": "Skipped",
|
||||
"translated": "Übersprungen"
|
||||
},
|
||||
{
|
||||
"id": "native.android.1a0e6b33059a63aa",
|
||||
"source": "Expired",
|
||||
"translated": "Abgelaufen"
|
||||
},
|
||||
{
|
||||
"id": "native.android.86c8d6113d07ef82",
|
||||
"source": "Unavailable",
|
||||
"translated": "Nicht verfügbar"
|
||||
},
|
||||
{
|
||||
"id": "native.android.f68cd7a84be4089e",
|
||||
"source": "Answered",
|
||||
@@ -7403,36 +7428,6 @@
|
||||
"source": "Answered elsewhere",
|
||||
"translated": "Anderswo beantwortet"
|
||||
},
|
||||
{
|
||||
"id": "native.android.1a0e6b33059a63aa",
|
||||
"source": "Expired",
|
||||
"translated": "Abgelaufen"
|
||||
},
|
||||
{
|
||||
"id": "native.android.d6d8fc83cec68418",
|
||||
"source": "Cancelled",
|
||||
"translated": "Abgebrochen"
|
||||
},
|
||||
{
|
||||
"id": "native.android.73f64d0d54b7f9c0",
|
||||
"source": "Pending",
|
||||
"translated": "Ausstehend"
|
||||
},
|
||||
{
|
||||
"id": "native.android.92dfa7979d04989c",
|
||||
"source": "Submitting…",
|
||||
"translated": "Wird gesendet…"
|
||||
},
|
||||
{
|
||||
"id": "native.android.c5e9ee99f9d9c612",
|
||||
"source": "Expires in ${seconds / 60}m ${seconds % 60}s",
|
||||
"translated": "Läuft in ${seconds / 60}m ${seconds % 60}s ab"
|
||||
},
|
||||
{
|
||||
"id": "native.android.2ca602fbdf96f1e7",
|
||||
"source": "Expires in ${seconds}s",
|
||||
"translated": "Läuft in ${seconds}s ab"
|
||||
},
|
||||
{
|
||||
"id": "native.android.118e8d239b18895e",
|
||||
"source": "Model",
|
||||
@@ -23038,6 +23033,41 @@
|
||||
"source": "Expand plan",
|
||||
"translated": "Plan ausklappen"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.ce899320a84d4086",
|
||||
"source": "Answered",
|
||||
"translated": "Beantwortet"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f019b6a07d5a33d8",
|
||||
"source": "Answered elsewhere",
|
||||
"translated": "Anderswo beantwortet"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.ebdedeffad570c43",
|
||||
"source": "Skipped",
|
||||
"translated": "Übersprungen"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.8dde2eaaf6b0dfa9",
|
||||
"source": "Expired",
|
||||
"translated": "Abgelaufen"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6dff9e9194347f3b",
|
||||
"source": "Unavailable",
|
||||
"translated": "Nicht verfügbar"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b34cd890366824f0",
|
||||
"source": "Pending",
|
||||
"translated": "Ausstehend"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b97da55b7e16e60c",
|
||||
"source": "Question summary",
|
||||
"translated": "Zusammenfassung der Frage"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.da4494f9e875169d",
|
||||
"source": "Other answer",
|
||||
@@ -23054,9 +23084,14 @@
|
||||
"translated": "Ausgewählt"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.e3095b651fc46b52",
|
||||
"source": "Submit",
|
||||
"translated": "Absenden"
|
||||
"id": "native.apple.1bbbbfd1ad3842ce",
|
||||
"source": "Skipping…",
|
||||
"translated": "Wird übersprungen…"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.14a4bf7b6b7d40dc",
|
||||
"source": "Skip",
|
||||
"translated": "Überspringen"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6c1adc16e0a846e5",
|
||||
@@ -23064,14 +23099,9 @@
|
||||
"translated": "Wird gesendet…"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.698bdb0d6537edb6",
|
||||
"source": "Expires in \\(seconds / 60)m \\(seconds % 60)s",
|
||||
"translated": "Läuft in \\(seconds / 60)m \\(seconds % 60)s ab"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.30e1c342b3345290",
|
||||
"source": "Expires in \\(seconds)s",
|
||||
"translated": "Läuft in \\(seconds)s ab"
|
||||
"id": "native.apple.e3095b651fc46b52",
|
||||
"source": "Submit",
|
||||
"translated": "Absenden"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7cd6fb01b9f5d14a",
|
||||
|
||||
+71
-41
@@ -7388,11 +7388,36 @@
|
||||
"source": "Other answer",
|
||||
"translated": "Otra respuesta"
|
||||
},
|
||||
{
|
||||
"id": "native.android.de591e6e2e440ecb",
|
||||
"source": "Skip",
|
||||
"translated": "Omitir"
|
||||
},
|
||||
{
|
||||
"id": "native.android.92dfa7979d04989c",
|
||||
"source": "Submitting…",
|
||||
"translated": "Enviando…"
|
||||
},
|
||||
{
|
||||
"id": "native.android.fa81739f52e7697f",
|
||||
"source": "Submit",
|
||||
"translated": "Enviar"
|
||||
},
|
||||
{
|
||||
"id": "native.android.7da074848cacfe4b",
|
||||
"source": "Skipped",
|
||||
"translated": "Omitido"
|
||||
},
|
||||
{
|
||||
"id": "native.android.1a0e6b33059a63aa",
|
||||
"source": "Expired",
|
||||
"translated": "Caducada"
|
||||
},
|
||||
{
|
||||
"id": "native.android.86c8d6113d07ef82",
|
||||
"source": "Unavailable",
|
||||
"translated": "No disponible"
|
||||
},
|
||||
{
|
||||
"id": "native.android.f68cd7a84be4089e",
|
||||
"source": "Answered",
|
||||
@@ -7403,36 +7428,6 @@
|
||||
"source": "Answered elsewhere",
|
||||
"translated": "Respondida en otro lugar"
|
||||
},
|
||||
{
|
||||
"id": "native.android.1a0e6b33059a63aa",
|
||||
"source": "Expired",
|
||||
"translated": "Caducada"
|
||||
},
|
||||
{
|
||||
"id": "native.android.d6d8fc83cec68418",
|
||||
"source": "Cancelled",
|
||||
"translated": "Cancelada"
|
||||
},
|
||||
{
|
||||
"id": "native.android.73f64d0d54b7f9c0",
|
||||
"source": "Pending",
|
||||
"translated": "Pendiente"
|
||||
},
|
||||
{
|
||||
"id": "native.android.92dfa7979d04989c",
|
||||
"source": "Submitting…",
|
||||
"translated": "Enviando…"
|
||||
},
|
||||
{
|
||||
"id": "native.android.c5e9ee99f9d9c612",
|
||||
"source": "Expires in ${seconds / 60}m ${seconds % 60}s",
|
||||
"translated": "Caduca en ${seconds / 60}m ${seconds % 60}s"
|
||||
},
|
||||
{
|
||||
"id": "native.android.2ca602fbdf96f1e7",
|
||||
"source": "Expires in ${seconds}s",
|
||||
"translated": "Caduca en ${seconds}s"
|
||||
},
|
||||
{
|
||||
"id": "native.android.118e8d239b18895e",
|
||||
"source": "Model",
|
||||
@@ -23038,6 +23033,41 @@
|
||||
"source": "Expand plan",
|
||||
"translated": "Expandir el plan"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.ce899320a84d4086",
|
||||
"source": "Answered",
|
||||
"translated": "Respondida"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f019b6a07d5a33d8",
|
||||
"source": "Answered elsewhere",
|
||||
"translated": "Respondida en otro lugar"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.ebdedeffad570c43",
|
||||
"source": "Skipped",
|
||||
"translated": "Omitido"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.8dde2eaaf6b0dfa9",
|
||||
"source": "Expired",
|
||||
"translated": "Caducada"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6dff9e9194347f3b",
|
||||
"source": "Unavailable",
|
||||
"translated": "No disponible"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b34cd890366824f0",
|
||||
"source": "Pending",
|
||||
"translated": "Pendiente"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b97da55b7e16e60c",
|
||||
"source": "Question summary",
|
||||
"translated": "Resumen de la pregunta"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.da4494f9e875169d",
|
||||
"source": "Other answer",
|
||||
@@ -23054,9 +23084,14 @@
|
||||
"translated": "Seleccionada"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.e3095b651fc46b52",
|
||||
"source": "Submit",
|
||||
"translated": "Enviar"
|
||||
"id": "native.apple.1bbbbfd1ad3842ce",
|
||||
"source": "Skipping…",
|
||||
"translated": "Omitiendo…"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.14a4bf7b6b7d40dc",
|
||||
"source": "Skip",
|
||||
"translated": "Omitir"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6c1adc16e0a846e5",
|
||||
@@ -23064,14 +23099,9 @@
|
||||
"translated": "Enviando…"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.698bdb0d6537edb6",
|
||||
"source": "Expires in \\(seconds / 60)m \\(seconds % 60)s",
|
||||
"translated": "Caduca en \\(seconds / 60)m \\(seconds % 60)s"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.30e1c342b3345290",
|
||||
"source": "Expires in \\(seconds)s",
|
||||
"translated": "Caduca en \\(seconds)s"
|
||||
"id": "native.apple.e3095b651fc46b52",
|
||||
"source": "Submit",
|
||||
"translated": "Enviar"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7cd6fb01b9f5d14a",
|
||||
|
||||
+71
-41
@@ -7388,11 +7388,36 @@
|
||||
"source": "Other answer",
|
||||
"translated": "پاسخ دیگر"
|
||||
},
|
||||
{
|
||||
"id": "native.android.de591e6e2e440ecb",
|
||||
"source": "Skip",
|
||||
"translated": "رد کردن"
|
||||
},
|
||||
{
|
||||
"id": "native.android.92dfa7979d04989c",
|
||||
"source": "Submitting…",
|
||||
"translated": "در حال ارسال…"
|
||||
},
|
||||
{
|
||||
"id": "native.android.fa81739f52e7697f",
|
||||
"source": "Submit",
|
||||
"translated": "ارسال"
|
||||
},
|
||||
{
|
||||
"id": "native.android.7da074848cacfe4b",
|
||||
"source": "Skipped",
|
||||
"translated": "رد شد"
|
||||
},
|
||||
{
|
||||
"id": "native.android.1a0e6b33059a63aa",
|
||||
"source": "Expired",
|
||||
"translated": "منقضی شده"
|
||||
},
|
||||
{
|
||||
"id": "native.android.86c8d6113d07ef82",
|
||||
"source": "Unavailable",
|
||||
"translated": "در دسترس نیست"
|
||||
},
|
||||
{
|
||||
"id": "native.android.f68cd7a84be4089e",
|
||||
"source": "Answered",
|
||||
@@ -7403,36 +7428,6 @@
|
||||
"source": "Answered elsewhere",
|
||||
"translated": "در جای دیگری پاسخ داده شده"
|
||||
},
|
||||
{
|
||||
"id": "native.android.1a0e6b33059a63aa",
|
||||
"source": "Expired",
|
||||
"translated": "منقضی شده"
|
||||
},
|
||||
{
|
||||
"id": "native.android.d6d8fc83cec68418",
|
||||
"source": "Cancelled",
|
||||
"translated": "لغو شده"
|
||||
},
|
||||
{
|
||||
"id": "native.android.73f64d0d54b7f9c0",
|
||||
"source": "Pending",
|
||||
"translated": "در انتظار"
|
||||
},
|
||||
{
|
||||
"id": "native.android.92dfa7979d04989c",
|
||||
"source": "Submitting…",
|
||||
"translated": "در حال ارسال…"
|
||||
},
|
||||
{
|
||||
"id": "native.android.c5e9ee99f9d9c612",
|
||||
"source": "Expires in ${seconds / 60}m ${seconds % 60}s",
|
||||
"translated": "انقضا تا ${seconds / 60}m ${seconds % 60}s دیگر"
|
||||
},
|
||||
{
|
||||
"id": "native.android.2ca602fbdf96f1e7",
|
||||
"source": "Expires in ${seconds}s",
|
||||
"translated": "انقضا تا ${seconds}s دیگر"
|
||||
},
|
||||
{
|
||||
"id": "native.android.118e8d239b18895e",
|
||||
"source": "Model",
|
||||
@@ -23038,6 +23033,41 @@
|
||||
"source": "Expand plan",
|
||||
"translated": "بازکردن برنامه"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.ce899320a84d4086",
|
||||
"source": "Answered",
|
||||
"translated": "پاسخ داده شده"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f019b6a07d5a33d8",
|
||||
"source": "Answered elsewhere",
|
||||
"translated": "در جای دیگری پاسخ داده شده"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.ebdedeffad570c43",
|
||||
"source": "Skipped",
|
||||
"translated": "رد شد"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.8dde2eaaf6b0dfa9",
|
||||
"source": "Expired",
|
||||
"translated": "منقضی شده"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6dff9e9194347f3b",
|
||||
"source": "Unavailable",
|
||||
"translated": "در دسترس نیست"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b34cd890366824f0",
|
||||
"source": "Pending",
|
||||
"translated": "در انتظار"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b97da55b7e16e60c",
|
||||
"source": "Question summary",
|
||||
"translated": "خلاصه پرسش"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.da4494f9e875169d",
|
||||
"source": "Other answer",
|
||||
@@ -23054,9 +23084,14 @@
|
||||
"translated": "انتخاب شده"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.e3095b651fc46b52",
|
||||
"source": "Submit",
|
||||
"translated": "ارسال"
|
||||
"id": "native.apple.1bbbbfd1ad3842ce",
|
||||
"source": "Skipping…",
|
||||
"translated": "در حال رد کردن…"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.14a4bf7b6b7d40dc",
|
||||
"source": "Skip",
|
||||
"translated": "رد کردن"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6c1adc16e0a846e5",
|
||||
@@ -23064,14 +23099,9 @@
|
||||
"translated": "در حال ارسال…"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.698bdb0d6537edb6",
|
||||
"source": "Expires in \\(seconds / 60)m \\(seconds % 60)s",
|
||||
"translated": "انقضا تا \\(seconds / 60)m \\(seconds % 60)s دیگر"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.30e1c342b3345290",
|
||||
"source": "Expires in \\(seconds)s",
|
||||
"translated": "انقضا تا \\(seconds)s دیگر"
|
||||
"id": "native.apple.e3095b651fc46b52",
|
||||
"source": "Submit",
|
||||
"translated": "ارسال"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7cd6fb01b9f5d14a",
|
||||
|
||||
+71
-41
@@ -7388,11 +7388,36 @@
|
||||
"source": "Other answer",
|
||||
"translated": "Autre réponse"
|
||||
},
|
||||
{
|
||||
"id": "native.android.de591e6e2e440ecb",
|
||||
"source": "Skip",
|
||||
"translated": "Ignorer"
|
||||
},
|
||||
{
|
||||
"id": "native.android.92dfa7979d04989c",
|
||||
"source": "Submitting…",
|
||||
"translated": "Envoi…"
|
||||
},
|
||||
{
|
||||
"id": "native.android.fa81739f52e7697f",
|
||||
"source": "Submit",
|
||||
"translated": "Envoyer"
|
||||
},
|
||||
{
|
||||
"id": "native.android.7da074848cacfe4b",
|
||||
"source": "Skipped",
|
||||
"translated": "Ignoré"
|
||||
},
|
||||
{
|
||||
"id": "native.android.1a0e6b33059a63aa",
|
||||
"source": "Expired",
|
||||
"translated": "Expiré"
|
||||
},
|
||||
{
|
||||
"id": "native.android.86c8d6113d07ef82",
|
||||
"source": "Unavailable",
|
||||
"translated": "Indisponible"
|
||||
},
|
||||
{
|
||||
"id": "native.android.f68cd7a84be4089e",
|
||||
"source": "Answered",
|
||||
@@ -7403,36 +7428,6 @@
|
||||
"source": "Answered elsewhere",
|
||||
"translated": "Répondu ailleurs"
|
||||
},
|
||||
{
|
||||
"id": "native.android.1a0e6b33059a63aa",
|
||||
"source": "Expired",
|
||||
"translated": "Expiré"
|
||||
},
|
||||
{
|
||||
"id": "native.android.d6d8fc83cec68418",
|
||||
"source": "Cancelled",
|
||||
"translated": "Annulé"
|
||||
},
|
||||
{
|
||||
"id": "native.android.73f64d0d54b7f9c0",
|
||||
"source": "Pending",
|
||||
"translated": "En attente"
|
||||
},
|
||||
{
|
||||
"id": "native.android.92dfa7979d04989c",
|
||||
"source": "Submitting…",
|
||||
"translated": "Envoi…"
|
||||
},
|
||||
{
|
||||
"id": "native.android.c5e9ee99f9d9c612",
|
||||
"source": "Expires in ${seconds / 60}m ${seconds % 60}s",
|
||||
"translated": "Expire dans ${seconds / 60} min ${seconds % 60} s"
|
||||
},
|
||||
{
|
||||
"id": "native.android.2ca602fbdf96f1e7",
|
||||
"source": "Expires in ${seconds}s",
|
||||
"translated": "Expire dans ${seconds} s"
|
||||
},
|
||||
{
|
||||
"id": "native.android.118e8d239b18895e",
|
||||
"source": "Model",
|
||||
@@ -23038,6 +23033,41 @@
|
||||
"source": "Expand plan",
|
||||
"translated": "Développer le plan"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.ce899320a84d4086",
|
||||
"source": "Answered",
|
||||
"translated": "Répondu"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f019b6a07d5a33d8",
|
||||
"source": "Answered elsewhere",
|
||||
"translated": "Répondu ailleurs"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.ebdedeffad570c43",
|
||||
"source": "Skipped",
|
||||
"translated": "Ignoré"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.8dde2eaaf6b0dfa9",
|
||||
"source": "Expired",
|
||||
"translated": "Expiré"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6dff9e9194347f3b",
|
||||
"source": "Unavailable",
|
||||
"translated": "Indisponible"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b34cd890366824f0",
|
||||
"source": "Pending",
|
||||
"translated": "En attente"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b97da55b7e16e60c",
|
||||
"source": "Question summary",
|
||||
"translated": "Résumé de la question"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.da4494f9e875169d",
|
||||
"source": "Other answer",
|
||||
@@ -23054,9 +23084,14 @@
|
||||
"translated": "Sélectionné"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.e3095b651fc46b52",
|
||||
"source": "Submit",
|
||||
"translated": "Envoyer"
|
||||
"id": "native.apple.1bbbbfd1ad3842ce",
|
||||
"source": "Skipping…",
|
||||
"translated": "Ignoré…"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.14a4bf7b6b7d40dc",
|
||||
"source": "Skip",
|
||||
"translated": "Ignorer"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6c1adc16e0a846e5",
|
||||
@@ -23064,14 +23099,9 @@
|
||||
"translated": "Envoi…"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.698bdb0d6537edb6",
|
||||
"source": "Expires in \\(seconds / 60)m \\(seconds % 60)s",
|
||||
"translated": "Expire dans \\(seconds / 60) min \\(seconds % 60) s"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.30e1c342b3345290",
|
||||
"source": "Expires in \\(seconds)s",
|
||||
"translated": "Expire dans \\(seconds) s"
|
||||
"id": "native.apple.e3095b651fc46b52",
|
||||
"source": "Submit",
|
||||
"translated": "Envoyer"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7cd6fb01b9f5d14a",
|
||||
|
||||
+71
-41
@@ -7388,11 +7388,36 @@
|
||||
"source": "Other answer",
|
||||
"translated": "अन्य उत्तर"
|
||||
},
|
||||
{
|
||||
"id": "native.android.de591e6e2e440ecb",
|
||||
"source": "Skip",
|
||||
"translated": "छोड़ें"
|
||||
},
|
||||
{
|
||||
"id": "native.android.92dfa7979d04989c",
|
||||
"source": "Submitting…",
|
||||
"translated": "सबमिट किया जा रहा है…"
|
||||
},
|
||||
{
|
||||
"id": "native.android.fa81739f52e7697f",
|
||||
"source": "Submit",
|
||||
"translated": "सबमिट करें"
|
||||
},
|
||||
{
|
||||
"id": "native.android.7da074848cacfe4b",
|
||||
"source": "Skipped",
|
||||
"translated": "छोड़ा गया"
|
||||
},
|
||||
{
|
||||
"id": "native.android.1a0e6b33059a63aa",
|
||||
"source": "Expired",
|
||||
"translated": "समय समाप्त"
|
||||
},
|
||||
{
|
||||
"id": "native.android.86c8d6113d07ef82",
|
||||
"source": "Unavailable",
|
||||
"translated": "अनुपलब्ध"
|
||||
},
|
||||
{
|
||||
"id": "native.android.f68cd7a84be4089e",
|
||||
"source": "Answered",
|
||||
@@ -7403,36 +7428,6 @@
|
||||
"source": "Answered elsewhere",
|
||||
"translated": "कहीं और उत्तर दिया गया"
|
||||
},
|
||||
{
|
||||
"id": "native.android.1a0e6b33059a63aa",
|
||||
"source": "Expired",
|
||||
"translated": "समय समाप्त"
|
||||
},
|
||||
{
|
||||
"id": "native.android.d6d8fc83cec68418",
|
||||
"source": "Cancelled",
|
||||
"translated": "रद्द किया गया"
|
||||
},
|
||||
{
|
||||
"id": "native.android.73f64d0d54b7f9c0",
|
||||
"source": "Pending",
|
||||
"translated": "लंबित"
|
||||
},
|
||||
{
|
||||
"id": "native.android.92dfa7979d04989c",
|
||||
"source": "Submitting…",
|
||||
"translated": "सबमिट किया जा रहा है…"
|
||||
},
|
||||
{
|
||||
"id": "native.android.c5e9ee99f9d9c612",
|
||||
"source": "Expires in ${seconds / 60}m ${seconds % 60}s",
|
||||
"translated": "${seconds / 60}m ${seconds % 60}s में समाप्त होगा"
|
||||
},
|
||||
{
|
||||
"id": "native.android.2ca602fbdf96f1e7",
|
||||
"source": "Expires in ${seconds}s",
|
||||
"translated": "${seconds}s में समाप्त होगा"
|
||||
},
|
||||
{
|
||||
"id": "native.android.118e8d239b18895e",
|
||||
"source": "Model",
|
||||
@@ -23038,6 +23033,41 @@
|
||||
"source": "Expand plan",
|
||||
"translated": "योजना विस्तृत करें"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.ce899320a84d4086",
|
||||
"source": "Answered",
|
||||
"translated": "उत्तर दिया गया"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f019b6a07d5a33d8",
|
||||
"source": "Answered elsewhere",
|
||||
"translated": "कहीं और उत्तर दिया गया"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.ebdedeffad570c43",
|
||||
"source": "Skipped",
|
||||
"translated": "छोड़ा गया"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.8dde2eaaf6b0dfa9",
|
||||
"source": "Expired",
|
||||
"translated": "समय समाप्त"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6dff9e9194347f3b",
|
||||
"source": "Unavailable",
|
||||
"translated": "अनुपलब्ध"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b34cd890366824f0",
|
||||
"source": "Pending",
|
||||
"translated": "लंबित"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b97da55b7e16e60c",
|
||||
"source": "Question summary",
|
||||
"translated": "प्रश्न का सारांश"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.da4494f9e875169d",
|
||||
"source": "Other answer",
|
||||
@@ -23054,9 +23084,14 @@
|
||||
"translated": "चयनित"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.e3095b651fc46b52",
|
||||
"source": "Submit",
|
||||
"translated": "सबमिट करें"
|
||||
"id": "native.apple.1bbbbfd1ad3842ce",
|
||||
"source": "Skipping…",
|
||||
"translated": "छोड़ा जा रहा है…"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.14a4bf7b6b7d40dc",
|
||||
"source": "Skip",
|
||||
"translated": "छोड़ें"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6c1adc16e0a846e5",
|
||||
@@ -23064,14 +23099,9 @@
|
||||
"translated": "सबमिट किया जा रहा है…"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.698bdb0d6537edb6",
|
||||
"source": "Expires in \\(seconds / 60)m \\(seconds % 60)s",
|
||||
"translated": "\\(seconds / 60)m \\(seconds % 60)s में समाप्त होगा"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.30e1c342b3345290",
|
||||
"source": "Expires in \\(seconds)s",
|
||||
"translated": "\\(seconds)s में समाप्त होगा"
|
||||
"id": "native.apple.e3095b651fc46b52",
|
||||
"source": "Submit",
|
||||
"translated": "सबमिट करें"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7cd6fb01b9f5d14a",
|
||||
|
||||
+71
-41
@@ -7388,11 +7388,36 @@
|
||||
"source": "Other answer",
|
||||
"translated": "Jawaban lain"
|
||||
},
|
||||
{
|
||||
"id": "native.android.de591e6e2e440ecb",
|
||||
"source": "Skip",
|
||||
"translated": "Lewati"
|
||||
},
|
||||
{
|
||||
"id": "native.android.92dfa7979d04989c",
|
||||
"source": "Submitting…",
|
||||
"translated": "Mengirim…"
|
||||
},
|
||||
{
|
||||
"id": "native.android.fa81739f52e7697f",
|
||||
"source": "Submit",
|
||||
"translated": "Kirim"
|
||||
},
|
||||
{
|
||||
"id": "native.android.7da074848cacfe4b",
|
||||
"source": "Skipped",
|
||||
"translated": "Dilewati"
|
||||
},
|
||||
{
|
||||
"id": "native.android.1a0e6b33059a63aa",
|
||||
"source": "Expired",
|
||||
"translated": "Kedaluwarsa"
|
||||
},
|
||||
{
|
||||
"id": "native.android.86c8d6113d07ef82",
|
||||
"source": "Unavailable",
|
||||
"translated": "Tidak tersedia"
|
||||
},
|
||||
{
|
||||
"id": "native.android.f68cd7a84be4089e",
|
||||
"source": "Answered",
|
||||
@@ -7403,36 +7428,6 @@
|
||||
"source": "Answered elsewhere",
|
||||
"translated": "Dijawab di tempat lain"
|
||||
},
|
||||
{
|
||||
"id": "native.android.1a0e6b33059a63aa",
|
||||
"source": "Expired",
|
||||
"translated": "Kedaluwarsa"
|
||||
},
|
||||
{
|
||||
"id": "native.android.d6d8fc83cec68418",
|
||||
"source": "Cancelled",
|
||||
"translated": "Dibatalkan"
|
||||
},
|
||||
{
|
||||
"id": "native.android.73f64d0d54b7f9c0",
|
||||
"source": "Pending",
|
||||
"translated": "Menunggu"
|
||||
},
|
||||
{
|
||||
"id": "native.android.92dfa7979d04989c",
|
||||
"source": "Submitting…",
|
||||
"translated": "Mengirim…"
|
||||
},
|
||||
{
|
||||
"id": "native.android.c5e9ee99f9d9c612",
|
||||
"source": "Expires in ${seconds / 60}m ${seconds % 60}s",
|
||||
"translated": "Kedaluwarsa dalam ${seconds / 60}m ${seconds % 60}s"
|
||||
},
|
||||
{
|
||||
"id": "native.android.2ca602fbdf96f1e7",
|
||||
"source": "Expires in ${seconds}s",
|
||||
"translated": "Kedaluwarsa dalam ${seconds}s"
|
||||
},
|
||||
{
|
||||
"id": "native.android.118e8d239b18895e",
|
||||
"source": "Model",
|
||||
@@ -23038,6 +23033,41 @@
|
||||
"source": "Expand plan",
|
||||
"translated": "Luaskan rencana"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.ce899320a84d4086",
|
||||
"source": "Answered",
|
||||
"translated": "Sudah dijawab"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f019b6a07d5a33d8",
|
||||
"source": "Answered elsewhere",
|
||||
"translated": "Dijawab di tempat lain"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.ebdedeffad570c43",
|
||||
"source": "Skipped",
|
||||
"translated": "Dilewati"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.8dde2eaaf6b0dfa9",
|
||||
"source": "Expired",
|
||||
"translated": "Kedaluwarsa"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6dff9e9194347f3b",
|
||||
"source": "Unavailable",
|
||||
"translated": "Tidak tersedia"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b34cd890366824f0",
|
||||
"source": "Pending",
|
||||
"translated": "Menunggu"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b97da55b7e16e60c",
|
||||
"source": "Question summary",
|
||||
"translated": "Ringkasan pertanyaan"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.da4494f9e875169d",
|
||||
"source": "Other answer",
|
||||
@@ -23054,9 +23084,14 @@
|
||||
"translated": "Dipilih"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.e3095b651fc46b52",
|
||||
"source": "Submit",
|
||||
"translated": "Kirim"
|
||||
"id": "native.apple.1bbbbfd1ad3842ce",
|
||||
"source": "Skipping…",
|
||||
"translated": "Melewati…"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.14a4bf7b6b7d40dc",
|
||||
"source": "Skip",
|
||||
"translated": "Lewati"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6c1adc16e0a846e5",
|
||||
@@ -23064,14 +23099,9 @@
|
||||
"translated": "Mengirim…"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.698bdb0d6537edb6",
|
||||
"source": "Expires in \\(seconds / 60)m \\(seconds % 60)s",
|
||||
"translated": "Kedaluwarsa dalam \\(seconds / 60)m \\(seconds % 60)s"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.30e1c342b3345290",
|
||||
"source": "Expires in \\(seconds)s",
|
||||
"translated": "Kedaluwarsa dalam \\(seconds)s"
|
||||
"id": "native.apple.e3095b651fc46b52",
|
||||
"source": "Submit",
|
||||
"translated": "Kirim"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7cd6fb01b9f5d14a",
|
||||
|
||||
+71
-41
@@ -7388,11 +7388,36 @@
|
||||
"source": "Other answer",
|
||||
"translated": "Altra risposta"
|
||||
},
|
||||
{
|
||||
"id": "native.android.de591e6e2e440ecb",
|
||||
"source": "Skip",
|
||||
"translated": "Salta"
|
||||
},
|
||||
{
|
||||
"id": "native.android.92dfa7979d04989c",
|
||||
"source": "Submitting…",
|
||||
"translated": "Invio in corso…"
|
||||
},
|
||||
{
|
||||
"id": "native.android.fa81739f52e7697f",
|
||||
"source": "Submit",
|
||||
"translated": "Invia"
|
||||
},
|
||||
{
|
||||
"id": "native.android.7da074848cacfe4b",
|
||||
"source": "Skipped",
|
||||
"translated": "Saltato"
|
||||
},
|
||||
{
|
||||
"id": "native.android.1a0e6b33059a63aa",
|
||||
"source": "Expired",
|
||||
"translated": "Scaduto"
|
||||
},
|
||||
{
|
||||
"id": "native.android.86c8d6113d07ef82",
|
||||
"source": "Unavailable",
|
||||
"translated": "Non disponibile"
|
||||
},
|
||||
{
|
||||
"id": "native.android.f68cd7a84be4089e",
|
||||
"source": "Answered",
|
||||
@@ -7403,36 +7428,6 @@
|
||||
"source": "Answered elsewhere",
|
||||
"translated": "Risposto altrove"
|
||||
},
|
||||
{
|
||||
"id": "native.android.1a0e6b33059a63aa",
|
||||
"source": "Expired",
|
||||
"translated": "Scaduto"
|
||||
},
|
||||
{
|
||||
"id": "native.android.d6d8fc83cec68418",
|
||||
"source": "Cancelled",
|
||||
"translated": "Annullato"
|
||||
},
|
||||
{
|
||||
"id": "native.android.73f64d0d54b7f9c0",
|
||||
"source": "Pending",
|
||||
"translated": "In attesa"
|
||||
},
|
||||
{
|
||||
"id": "native.android.92dfa7979d04989c",
|
||||
"source": "Submitting…",
|
||||
"translated": "Invio in corso…"
|
||||
},
|
||||
{
|
||||
"id": "native.android.c5e9ee99f9d9c612",
|
||||
"source": "Expires in ${seconds / 60}m ${seconds % 60}s",
|
||||
"translated": "Scade tra ${seconds / 60}m ${seconds % 60}s"
|
||||
},
|
||||
{
|
||||
"id": "native.android.2ca602fbdf96f1e7",
|
||||
"source": "Expires in ${seconds}s",
|
||||
"translated": "Scade tra ${seconds}s"
|
||||
},
|
||||
{
|
||||
"id": "native.android.118e8d239b18895e",
|
||||
"source": "Model",
|
||||
@@ -23038,6 +23033,41 @@
|
||||
"source": "Expand plan",
|
||||
"translated": "Espandi il piano"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.ce899320a84d4086",
|
||||
"source": "Answered",
|
||||
"translated": "Risposto"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f019b6a07d5a33d8",
|
||||
"source": "Answered elsewhere",
|
||||
"translated": "Risposto altrove"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.ebdedeffad570c43",
|
||||
"source": "Skipped",
|
||||
"translated": "Saltato"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.8dde2eaaf6b0dfa9",
|
||||
"source": "Expired",
|
||||
"translated": "Scaduto"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6dff9e9194347f3b",
|
||||
"source": "Unavailable",
|
||||
"translated": "Non disponibile"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b34cd890366824f0",
|
||||
"source": "Pending",
|
||||
"translated": "In sospeso"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b97da55b7e16e60c",
|
||||
"source": "Question summary",
|
||||
"translated": "Riepilogo della domanda"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.da4494f9e875169d",
|
||||
"source": "Other answer",
|
||||
@@ -23054,9 +23084,14 @@
|
||||
"translated": "Selezionato"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.e3095b651fc46b52",
|
||||
"source": "Submit",
|
||||
"translated": "Invia"
|
||||
"id": "native.apple.1bbbbfd1ad3842ce",
|
||||
"source": "Skipping…",
|
||||
"translated": "Ignoro…"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.14a4bf7b6b7d40dc",
|
||||
"source": "Skip",
|
||||
"translated": "Salta"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6c1adc16e0a846e5",
|
||||
@@ -23064,14 +23099,9 @@
|
||||
"translated": "Invio in corso…"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.698bdb0d6537edb6",
|
||||
"source": "Expires in \\(seconds / 60)m \\(seconds % 60)s",
|
||||
"translated": "Scade tra \\(seconds / 60)m \\(seconds % 60)s"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.30e1c342b3345290",
|
||||
"source": "Expires in \\(seconds)s",
|
||||
"translated": "Scade tra \\(seconds)s"
|
||||
"id": "native.apple.e3095b651fc46b52",
|
||||
"source": "Submit",
|
||||
"translated": "Invia"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7cd6fb01b9f5d14a",
|
||||
|
||||
@@ -7388,11 +7388,36 @@
|
||||
"source": "Other answer",
|
||||
"translated": "その他の回答"
|
||||
},
|
||||
{
|
||||
"id": "native.android.de591e6e2e440ecb",
|
||||
"source": "Skip",
|
||||
"translated": "スキップ"
|
||||
},
|
||||
{
|
||||
"id": "native.android.92dfa7979d04989c",
|
||||
"source": "Submitting…",
|
||||
"translated": "送信中…"
|
||||
},
|
||||
{
|
||||
"id": "native.android.fa81739f52e7697f",
|
||||
"source": "Submit",
|
||||
"translated": "送信"
|
||||
},
|
||||
{
|
||||
"id": "native.android.7da074848cacfe4b",
|
||||
"source": "Skipped",
|
||||
"translated": "スキップ済み"
|
||||
},
|
||||
{
|
||||
"id": "native.android.1a0e6b33059a63aa",
|
||||
"source": "Expired",
|
||||
"translated": "期限切れ"
|
||||
},
|
||||
{
|
||||
"id": "native.android.86c8d6113d07ef82",
|
||||
"source": "Unavailable",
|
||||
"translated": "利用できません"
|
||||
},
|
||||
{
|
||||
"id": "native.android.f68cd7a84be4089e",
|
||||
"source": "Answered",
|
||||
@@ -7403,36 +7428,6 @@
|
||||
"source": "Answered elsewhere",
|
||||
"translated": "他で回答済み"
|
||||
},
|
||||
{
|
||||
"id": "native.android.1a0e6b33059a63aa",
|
||||
"source": "Expired",
|
||||
"translated": "期限切れ"
|
||||
},
|
||||
{
|
||||
"id": "native.android.d6d8fc83cec68418",
|
||||
"source": "Cancelled",
|
||||
"translated": "キャンセル済み"
|
||||
},
|
||||
{
|
||||
"id": "native.android.73f64d0d54b7f9c0",
|
||||
"source": "Pending",
|
||||
"translated": "保留中"
|
||||
},
|
||||
{
|
||||
"id": "native.android.92dfa7979d04989c",
|
||||
"source": "Submitting…",
|
||||
"translated": "送信中…"
|
||||
},
|
||||
{
|
||||
"id": "native.android.c5e9ee99f9d9c612",
|
||||
"source": "Expires in ${seconds / 60}m ${seconds % 60}s",
|
||||
"translated": "期限切れまで${seconds / 60}m ${seconds % 60}s"
|
||||
},
|
||||
{
|
||||
"id": "native.android.2ca602fbdf96f1e7",
|
||||
"source": "Expires in ${seconds}s",
|
||||
"translated": "期限切れまで${seconds}s"
|
||||
},
|
||||
{
|
||||
"id": "native.android.118e8d239b18895e",
|
||||
"source": "Model",
|
||||
@@ -23038,6 +23033,41 @@
|
||||
"source": "Expand plan",
|
||||
"translated": "計画を展開"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.ce899320a84d4086",
|
||||
"source": "Answered",
|
||||
"translated": "回答済み"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f019b6a07d5a33d8",
|
||||
"source": "Answered elsewhere",
|
||||
"translated": "他で回答済み"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.ebdedeffad570c43",
|
||||
"source": "Skipped",
|
||||
"translated": "スキップ済み"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.8dde2eaaf6b0dfa9",
|
||||
"source": "Expired",
|
||||
"translated": "期限切れ"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6dff9e9194347f3b",
|
||||
"source": "Unavailable",
|
||||
"translated": "利用できません"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b34cd890366824f0",
|
||||
"source": "Pending",
|
||||
"translated": "保留中"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b97da55b7e16e60c",
|
||||
"source": "Question summary",
|
||||
"translated": "質問の概要"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.da4494f9e875169d",
|
||||
"source": "Other answer",
|
||||
@@ -23054,9 +23084,14 @@
|
||||
"translated": "選択済み"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.e3095b651fc46b52",
|
||||
"source": "Submit",
|
||||
"translated": "送信"
|
||||
"id": "native.apple.1bbbbfd1ad3842ce",
|
||||
"source": "Skipping…",
|
||||
"translated": "スキップ中…"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.14a4bf7b6b7d40dc",
|
||||
"source": "Skip",
|
||||
"translated": "スキップ"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6c1adc16e0a846e5",
|
||||
@@ -23064,14 +23099,9 @@
|
||||
"translated": "送信中…"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.698bdb0d6537edb6",
|
||||
"source": "Expires in \\(seconds / 60)m \\(seconds % 60)s",
|
||||
"translated": "期限切れまで\\(seconds / 60)m \\(seconds % 60)s"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.30e1c342b3345290",
|
||||
"source": "Expires in \\(seconds)s",
|
||||
"translated": "期限切れまで\\(seconds)s"
|
||||
"id": "native.apple.e3095b651fc46b52",
|
||||
"source": "Submit",
|
||||
"translated": "送信"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7cd6fb01b9f5d14a",
|
||||
|
||||
+71
-41
@@ -7388,11 +7388,36 @@
|
||||
"source": "Other answer",
|
||||
"translated": "기타 답변"
|
||||
},
|
||||
{
|
||||
"id": "native.android.de591e6e2e440ecb",
|
||||
"source": "Skip",
|
||||
"translated": "건너뛰기"
|
||||
},
|
||||
{
|
||||
"id": "native.android.92dfa7979d04989c",
|
||||
"source": "Submitting…",
|
||||
"translated": "제출 중…"
|
||||
},
|
||||
{
|
||||
"id": "native.android.fa81739f52e7697f",
|
||||
"source": "Submit",
|
||||
"translated": "제출"
|
||||
},
|
||||
{
|
||||
"id": "native.android.7da074848cacfe4b",
|
||||
"source": "Skipped",
|
||||
"translated": "건너뜀"
|
||||
},
|
||||
{
|
||||
"id": "native.android.1a0e6b33059a63aa",
|
||||
"source": "Expired",
|
||||
"translated": "만료됨"
|
||||
},
|
||||
{
|
||||
"id": "native.android.86c8d6113d07ef82",
|
||||
"source": "Unavailable",
|
||||
"translated": "사용할 수 없음"
|
||||
},
|
||||
{
|
||||
"id": "native.android.f68cd7a84be4089e",
|
||||
"source": "Answered",
|
||||
@@ -7403,36 +7428,6 @@
|
||||
"source": "Answered elsewhere",
|
||||
"translated": "다른 곳에서 응답 완료"
|
||||
},
|
||||
{
|
||||
"id": "native.android.1a0e6b33059a63aa",
|
||||
"source": "Expired",
|
||||
"translated": "만료됨"
|
||||
},
|
||||
{
|
||||
"id": "native.android.d6d8fc83cec68418",
|
||||
"source": "Cancelled",
|
||||
"translated": "취소됨"
|
||||
},
|
||||
{
|
||||
"id": "native.android.73f64d0d54b7f9c0",
|
||||
"source": "Pending",
|
||||
"translated": "대기 중"
|
||||
},
|
||||
{
|
||||
"id": "native.android.92dfa7979d04989c",
|
||||
"source": "Submitting…",
|
||||
"translated": "제출 중…"
|
||||
},
|
||||
{
|
||||
"id": "native.android.c5e9ee99f9d9c612",
|
||||
"source": "Expires in ${seconds / 60}m ${seconds % 60}s",
|
||||
"translated": "${seconds / 60}분 ${seconds % 60}초 후 만료"
|
||||
},
|
||||
{
|
||||
"id": "native.android.2ca602fbdf96f1e7",
|
||||
"source": "Expires in ${seconds}s",
|
||||
"translated": "${seconds}초 후 만료"
|
||||
},
|
||||
{
|
||||
"id": "native.android.118e8d239b18895e",
|
||||
"source": "Model",
|
||||
@@ -23038,6 +23033,41 @@
|
||||
"source": "Expand plan",
|
||||
"translated": "계획 펼치기"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.ce899320a84d4086",
|
||||
"source": "Answered",
|
||||
"translated": "응답 완료"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f019b6a07d5a33d8",
|
||||
"source": "Answered elsewhere",
|
||||
"translated": "다른 곳에서 응답 완료"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.ebdedeffad570c43",
|
||||
"source": "Skipped",
|
||||
"translated": "건너뜀"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.8dde2eaaf6b0dfa9",
|
||||
"source": "Expired",
|
||||
"translated": "만료됨"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6dff9e9194347f3b",
|
||||
"source": "Unavailable",
|
||||
"translated": "사용할 수 없음"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b34cd890366824f0",
|
||||
"source": "Pending",
|
||||
"translated": "대기 중"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b97da55b7e16e60c",
|
||||
"source": "Question summary",
|
||||
"translated": "질문 요약"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.da4494f9e875169d",
|
||||
"source": "Other answer",
|
||||
@@ -23054,9 +23084,14 @@
|
||||
"translated": "선택됨"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.e3095b651fc46b52",
|
||||
"source": "Submit",
|
||||
"translated": "제출"
|
||||
"id": "native.apple.1bbbbfd1ad3842ce",
|
||||
"source": "Skipping…",
|
||||
"translated": "건너뛰는 중…"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.14a4bf7b6b7d40dc",
|
||||
"source": "Skip",
|
||||
"translated": "건너뛰기"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6c1adc16e0a846e5",
|
||||
@@ -23064,14 +23099,9 @@
|
||||
"translated": "제출 중…"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.698bdb0d6537edb6",
|
||||
"source": "Expires in \\(seconds / 60)m \\(seconds % 60)s",
|
||||
"translated": "\\(seconds / 60)분 \\(seconds % 60)초 후 만료"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.30e1c342b3345290",
|
||||
"source": "Expires in \\(seconds)s",
|
||||
"translated": "\\(seconds)초 후 만료"
|
||||
"id": "native.apple.e3095b651fc46b52",
|
||||
"source": "Submit",
|
||||
"translated": "제출"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7cd6fb01b9f5d14a",
|
||||
|
||||
+71
-41
@@ -7388,11 +7388,36 @@
|
||||
"source": "Other answer",
|
||||
"translated": "Ander antwoord"
|
||||
},
|
||||
{
|
||||
"id": "native.android.de591e6e2e440ecb",
|
||||
"source": "Skip",
|
||||
"translated": "Overslaan"
|
||||
},
|
||||
{
|
||||
"id": "native.android.92dfa7979d04989c",
|
||||
"source": "Submitting…",
|
||||
"translated": "Bezig met indienen…"
|
||||
},
|
||||
{
|
||||
"id": "native.android.fa81739f52e7697f",
|
||||
"source": "Submit",
|
||||
"translated": "Indienen"
|
||||
},
|
||||
{
|
||||
"id": "native.android.7da074848cacfe4b",
|
||||
"source": "Skipped",
|
||||
"translated": "Overgeslagen"
|
||||
},
|
||||
{
|
||||
"id": "native.android.1a0e6b33059a63aa",
|
||||
"source": "Expired",
|
||||
"translated": "Verlopen"
|
||||
},
|
||||
{
|
||||
"id": "native.android.86c8d6113d07ef82",
|
||||
"source": "Unavailable",
|
||||
"translated": "Niet beschikbaar"
|
||||
},
|
||||
{
|
||||
"id": "native.android.f68cd7a84be4089e",
|
||||
"source": "Answered",
|
||||
@@ -7403,36 +7428,6 @@
|
||||
"source": "Answered elsewhere",
|
||||
"translated": "Elders beantwoord"
|
||||
},
|
||||
{
|
||||
"id": "native.android.1a0e6b33059a63aa",
|
||||
"source": "Expired",
|
||||
"translated": "Verlopen"
|
||||
},
|
||||
{
|
||||
"id": "native.android.d6d8fc83cec68418",
|
||||
"source": "Cancelled",
|
||||
"translated": "Geannuleerd"
|
||||
},
|
||||
{
|
||||
"id": "native.android.73f64d0d54b7f9c0",
|
||||
"source": "Pending",
|
||||
"translated": "In afwachting"
|
||||
},
|
||||
{
|
||||
"id": "native.android.92dfa7979d04989c",
|
||||
"source": "Submitting…",
|
||||
"translated": "Bezig met indienen…"
|
||||
},
|
||||
{
|
||||
"id": "native.android.c5e9ee99f9d9c612",
|
||||
"source": "Expires in ${seconds / 60}m ${seconds % 60}s",
|
||||
"translated": "Verloopt over ${seconds / 60}m ${seconds % 60}s"
|
||||
},
|
||||
{
|
||||
"id": "native.android.2ca602fbdf96f1e7",
|
||||
"source": "Expires in ${seconds}s",
|
||||
"translated": "Verloopt over ${seconds}s"
|
||||
},
|
||||
{
|
||||
"id": "native.android.118e8d239b18895e",
|
||||
"source": "Model",
|
||||
@@ -23038,6 +23033,41 @@
|
||||
"source": "Expand plan",
|
||||
"translated": "Plan uitklappen"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.ce899320a84d4086",
|
||||
"source": "Answered",
|
||||
"translated": "Beantwoord"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f019b6a07d5a33d8",
|
||||
"source": "Answered elsewhere",
|
||||
"translated": "Elders beantwoord"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.ebdedeffad570c43",
|
||||
"source": "Skipped",
|
||||
"translated": "Overgeslagen"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.8dde2eaaf6b0dfa9",
|
||||
"source": "Expired",
|
||||
"translated": "Verlopen"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6dff9e9194347f3b",
|
||||
"source": "Unavailable",
|
||||
"translated": "Niet beschikbaar"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b34cd890366824f0",
|
||||
"source": "Pending",
|
||||
"translated": "In behandeling"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b97da55b7e16e60c",
|
||||
"source": "Question summary",
|
||||
"translated": "Samenvatting van de vraag"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.da4494f9e875169d",
|
||||
"source": "Other answer",
|
||||
@@ -23054,9 +23084,14 @@
|
||||
"translated": "Geselecteerd"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.e3095b651fc46b52",
|
||||
"source": "Submit",
|
||||
"translated": "Indienen"
|
||||
"id": "native.apple.1bbbbfd1ad3842ce",
|
||||
"source": "Skipping…",
|
||||
"translated": "Overslaan…"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.14a4bf7b6b7d40dc",
|
||||
"source": "Skip",
|
||||
"translated": "Overslaan"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6c1adc16e0a846e5",
|
||||
@@ -23064,14 +23099,9 @@
|
||||
"translated": "Bezig met indienen…"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.698bdb0d6537edb6",
|
||||
"source": "Expires in \\(seconds / 60)m \\(seconds % 60)s",
|
||||
"translated": "Verloopt over \\(seconds / 60)m \\(seconds % 60)s"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.30e1c342b3345290",
|
||||
"source": "Expires in \\(seconds)s",
|
||||
"translated": "Verloopt over \\(seconds)s"
|
||||
"id": "native.apple.e3095b651fc46b52",
|
||||
"source": "Submit",
|
||||
"translated": "Indienen"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7cd6fb01b9f5d14a",
|
||||
|
||||
+71
-41
@@ -7388,11 +7388,36 @@
|
||||
"source": "Other answer",
|
||||
"translated": "Inna odpowiedź"
|
||||
},
|
||||
{
|
||||
"id": "native.android.de591e6e2e440ecb",
|
||||
"source": "Skip",
|
||||
"translated": "Pomiń"
|
||||
},
|
||||
{
|
||||
"id": "native.android.92dfa7979d04989c",
|
||||
"source": "Submitting…",
|
||||
"translated": "Przesyłanie…"
|
||||
},
|
||||
{
|
||||
"id": "native.android.fa81739f52e7697f",
|
||||
"source": "Submit",
|
||||
"translated": "Prześlij"
|
||||
},
|
||||
{
|
||||
"id": "native.android.7da074848cacfe4b",
|
||||
"source": "Skipped",
|
||||
"translated": "Pominięto"
|
||||
},
|
||||
{
|
||||
"id": "native.android.1a0e6b33059a63aa",
|
||||
"source": "Expired",
|
||||
"translated": "Wygasło"
|
||||
},
|
||||
{
|
||||
"id": "native.android.86c8d6113d07ef82",
|
||||
"source": "Unavailable",
|
||||
"translated": "Niedostępne"
|
||||
},
|
||||
{
|
||||
"id": "native.android.f68cd7a84be4089e",
|
||||
"source": "Answered",
|
||||
@@ -7403,36 +7428,6 @@
|
||||
"source": "Answered elsewhere",
|
||||
"translated": "Odpowiedziano gdzie indziej"
|
||||
},
|
||||
{
|
||||
"id": "native.android.1a0e6b33059a63aa",
|
||||
"source": "Expired",
|
||||
"translated": "Wygasło"
|
||||
},
|
||||
{
|
||||
"id": "native.android.d6d8fc83cec68418",
|
||||
"source": "Cancelled",
|
||||
"translated": "Anulowano"
|
||||
},
|
||||
{
|
||||
"id": "native.android.73f64d0d54b7f9c0",
|
||||
"source": "Pending",
|
||||
"translated": "Oczekuje"
|
||||
},
|
||||
{
|
||||
"id": "native.android.92dfa7979d04989c",
|
||||
"source": "Submitting…",
|
||||
"translated": "Przesyłanie…"
|
||||
},
|
||||
{
|
||||
"id": "native.android.c5e9ee99f9d9c612",
|
||||
"source": "Expires in ${seconds / 60}m ${seconds % 60}s",
|
||||
"translated": "Wygasa za ${seconds / 60}m ${seconds % 60}s"
|
||||
},
|
||||
{
|
||||
"id": "native.android.2ca602fbdf96f1e7",
|
||||
"source": "Expires in ${seconds}s",
|
||||
"translated": "Wygasa za ${seconds}s"
|
||||
},
|
||||
{
|
||||
"id": "native.android.118e8d239b18895e",
|
||||
"source": "Model",
|
||||
@@ -23038,6 +23033,41 @@
|
||||
"source": "Expand plan",
|
||||
"translated": "Rozwiń plan"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.ce899320a84d4086",
|
||||
"source": "Answered",
|
||||
"translated": "Udzielono odpowiedzi"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f019b6a07d5a33d8",
|
||||
"source": "Answered elsewhere",
|
||||
"translated": "Odpowiedziano gdzie indziej"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.ebdedeffad570c43",
|
||||
"source": "Skipped",
|
||||
"translated": "Pominięto"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.8dde2eaaf6b0dfa9",
|
||||
"source": "Expired",
|
||||
"translated": "Wygasło"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6dff9e9194347f3b",
|
||||
"source": "Unavailable",
|
||||
"translated": "Niedostępne"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b34cd890366824f0",
|
||||
"source": "Pending",
|
||||
"translated": "Oczekujące"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b97da55b7e16e60c",
|
||||
"source": "Question summary",
|
||||
"translated": "Podsumowanie pytania"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.da4494f9e875169d",
|
||||
"source": "Other answer",
|
||||
@@ -23054,9 +23084,14 @@
|
||||
"translated": "Wybrano"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.e3095b651fc46b52",
|
||||
"source": "Submit",
|
||||
"translated": "Prześlij"
|
||||
"id": "native.apple.1bbbbfd1ad3842ce",
|
||||
"source": "Skipping…",
|
||||
"translated": "Pomijanie…"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.14a4bf7b6b7d40dc",
|
||||
"source": "Skip",
|
||||
"translated": "Pomiń"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6c1adc16e0a846e5",
|
||||
@@ -23064,14 +23099,9 @@
|
||||
"translated": "Przesyłanie…"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.698bdb0d6537edb6",
|
||||
"source": "Expires in \\(seconds / 60)m \\(seconds % 60)s",
|
||||
"translated": "Wygasa za \\(seconds / 60)m \\(seconds % 60)s"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.30e1c342b3345290",
|
||||
"source": "Expires in \\(seconds)s",
|
||||
"translated": "Wygasa za \\(seconds)s"
|
||||
"id": "native.apple.e3095b651fc46b52",
|
||||
"source": "Submit",
|
||||
"translated": "Prześlij"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7cd6fb01b9f5d14a",
|
||||
|
||||
@@ -7388,11 +7388,36 @@
|
||||
"source": "Other answer",
|
||||
"translated": "Outra resposta"
|
||||
},
|
||||
{
|
||||
"id": "native.android.de591e6e2e440ecb",
|
||||
"source": "Skip",
|
||||
"translated": "Pular"
|
||||
},
|
||||
{
|
||||
"id": "native.android.92dfa7979d04989c",
|
||||
"source": "Submitting…",
|
||||
"translated": "Enviando…"
|
||||
},
|
||||
{
|
||||
"id": "native.android.fa81739f52e7697f",
|
||||
"source": "Submit",
|
||||
"translated": "Enviar"
|
||||
},
|
||||
{
|
||||
"id": "native.android.7da074848cacfe4b",
|
||||
"source": "Skipped",
|
||||
"translated": "Pulado"
|
||||
},
|
||||
{
|
||||
"id": "native.android.1a0e6b33059a63aa",
|
||||
"source": "Expired",
|
||||
"translated": "Expirado"
|
||||
},
|
||||
{
|
||||
"id": "native.android.86c8d6113d07ef82",
|
||||
"source": "Unavailable",
|
||||
"translated": "Indisponível"
|
||||
},
|
||||
{
|
||||
"id": "native.android.f68cd7a84be4089e",
|
||||
"source": "Answered",
|
||||
@@ -7403,36 +7428,6 @@
|
||||
"source": "Answered elsewhere",
|
||||
"translated": "Respondido em outro lugar"
|
||||
},
|
||||
{
|
||||
"id": "native.android.1a0e6b33059a63aa",
|
||||
"source": "Expired",
|
||||
"translated": "Expirado"
|
||||
},
|
||||
{
|
||||
"id": "native.android.d6d8fc83cec68418",
|
||||
"source": "Cancelled",
|
||||
"translated": "Cancelado"
|
||||
},
|
||||
{
|
||||
"id": "native.android.73f64d0d54b7f9c0",
|
||||
"source": "Pending",
|
||||
"translated": "Pendente"
|
||||
},
|
||||
{
|
||||
"id": "native.android.92dfa7979d04989c",
|
||||
"source": "Submitting…",
|
||||
"translated": "Enviando…"
|
||||
},
|
||||
{
|
||||
"id": "native.android.c5e9ee99f9d9c612",
|
||||
"source": "Expires in ${seconds / 60}m ${seconds % 60}s",
|
||||
"translated": "Expira em ${seconds / 60}m ${seconds % 60}s"
|
||||
},
|
||||
{
|
||||
"id": "native.android.2ca602fbdf96f1e7",
|
||||
"source": "Expires in ${seconds}s",
|
||||
"translated": "Expira em ${seconds}s"
|
||||
},
|
||||
{
|
||||
"id": "native.android.118e8d239b18895e",
|
||||
"source": "Model",
|
||||
@@ -23038,6 +23033,41 @@
|
||||
"source": "Expand plan",
|
||||
"translated": "Expandir plano"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.ce899320a84d4086",
|
||||
"source": "Answered",
|
||||
"translated": "Respondido"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f019b6a07d5a33d8",
|
||||
"source": "Answered elsewhere",
|
||||
"translated": "Respondido em outro lugar"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.ebdedeffad570c43",
|
||||
"source": "Skipped",
|
||||
"translated": "Pulado"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.8dde2eaaf6b0dfa9",
|
||||
"source": "Expired",
|
||||
"translated": "Expirado"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6dff9e9194347f3b",
|
||||
"source": "Unavailable",
|
||||
"translated": "Indisponível"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b34cd890366824f0",
|
||||
"source": "Pending",
|
||||
"translated": "Pendente"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b97da55b7e16e60c",
|
||||
"source": "Question summary",
|
||||
"translated": "Resumo da pergunta"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.da4494f9e875169d",
|
||||
"source": "Other answer",
|
||||
@@ -23054,9 +23084,14 @@
|
||||
"translated": "Selecionado"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.e3095b651fc46b52",
|
||||
"source": "Submit",
|
||||
"translated": "Enviar"
|
||||
"id": "native.apple.1bbbbfd1ad3842ce",
|
||||
"source": "Skipping…",
|
||||
"translated": "Pulando…"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.14a4bf7b6b7d40dc",
|
||||
"source": "Skip",
|
||||
"translated": "Pular"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6c1adc16e0a846e5",
|
||||
@@ -23064,14 +23099,9 @@
|
||||
"translated": "Enviando…"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.698bdb0d6537edb6",
|
||||
"source": "Expires in \\(seconds / 60)m \\(seconds % 60)s",
|
||||
"translated": "Expira em \\(seconds / 60)m \\(seconds % 60)s"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.30e1c342b3345290",
|
||||
"source": "Expires in \\(seconds)s",
|
||||
"translated": "Expira em \\(seconds)s"
|
||||
"id": "native.apple.e3095b651fc46b52",
|
||||
"source": "Submit",
|
||||
"translated": "Enviar"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7cd6fb01b9f5d14a",
|
||||
|
||||
+71
-41
@@ -7388,11 +7388,36 @@
|
||||
"source": "Other answer",
|
||||
"translated": "Другой ответ"
|
||||
},
|
||||
{
|
||||
"id": "native.android.de591e6e2e440ecb",
|
||||
"source": "Skip",
|
||||
"translated": "Пропустить"
|
||||
},
|
||||
{
|
||||
"id": "native.android.92dfa7979d04989c",
|
||||
"source": "Submitting…",
|
||||
"translated": "Отправка…"
|
||||
},
|
||||
{
|
||||
"id": "native.android.fa81739f52e7697f",
|
||||
"source": "Submit",
|
||||
"translated": "Отправить"
|
||||
},
|
||||
{
|
||||
"id": "native.android.7da074848cacfe4b",
|
||||
"source": "Skipped",
|
||||
"translated": "Пропущено"
|
||||
},
|
||||
{
|
||||
"id": "native.android.1a0e6b33059a63aa",
|
||||
"source": "Expired",
|
||||
"translated": "Срок истёк"
|
||||
},
|
||||
{
|
||||
"id": "native.android.86c8d6113d07ef82",
|
||||
"source": "Unavailable",
|
||||
"translated": "Недоступно"
|
||||
},
|
||||
{
|
||||
"id": "native.android.f68cd7a84be4089e",
|
||||
"source": "Answered",
|
||||
@@ -7403,36 +7428,6 @@
|
||||
"source": "Answered elsewhere",
|
||||
"translated": "Отвечено в другом месте"
|
||||
},
|
||||
{
|
||||
"id": "native.android.1a0e6b33059a63aa",
|
||||
"source": "Expired",
|
||||
"translated": "Срок истёк"
|
||||
},
|
||||
{
|
||||
"id": "native.android.d6d8fc83cec68418",
|
||||
"source": "Cancelled",
|
||||
"translated": "Отменено"
|
||||
},
|
||||
{
|
||||
"id": "native.android.73f64d0d54b7f9c0",
|
||||
"source": "Pending",
|
||||
"translated": "Ожидает ответа"
|
||||
},
|
||||
{
|
||||
"id": "native.android.92dfa7979d04989c",
|
||||
"source": "Submitting…",
|
||||
"translated": "Отправка…"
|
||||
},
|
||||
{
|
||||
"id": "native.android.c5e9ee99f9d9c612",
|
||||
"source": "Expires in ${seconds / 60}m ${seconds % 60}s",
|
||||
"translated": "Истекает через ${seconds / 60} мин ${seconds % 60} с"
|
||||
},
|
||||
{
|
||||
"id": "native.android.2ca602fbdf96f1e7",
|
||||
"source": "Expires in ${seconds}s",
|
||||
"translated": "Истекает через ${seconds} с"
|
||||
},
|
||||
{
|
||||
"id": "native.android.118e8d239b18895e",
|
||||
"source": "Model",
|
||||
@@ -23038,6 +23033,41 @@
|
||||
"source": "Expand plan",
|
||||
"translated": "Развернуть план"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.ce899320a84d4086",
|
||||
"source": "Answered",
|
||||
"translated": "Отвечено"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f019b6a07d5a33d8",
|
||||
"source": "Answered elsewhere",
|
||||
"translated": "Отвечено в другом месте"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.ebdedeffad570c43",
|
||||
"source": "Skipped",
|
||||
"translated": "Пропущено"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.8dde2eaaf6b0dfa9",
|
||||
"source": "Expired",
|
||||
"translated": "Срок истёк"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6dff9e9194347f3b",
|
||||
"source": "Unavailable",
|
||||
"translated": "Недоступно"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b34cd890366824f0",
|
||||
"source": "Pending",
|
||||
"translated": "Ожидание"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b97da55b7e16e60c",
|
||||
"source": "Question summary",
|
||||
"translated": "Краткое описание вопроса"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.da4494f9e875169d",
|
||||
"source": "Other answer",
|
||||
@@ -23054,9 +23084,14 @@
|
||||
"translated": "Выбрано"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.e3095b651fc46b52",
|
||||
"source": "Submit",
|
||||
"translated": "Отправить"
|
||||
"id": "native.apple.1bbbbfd1ad3842ce",
|
||||
"source": "Skipping…",
|
||||
"translated": "Пропуск…"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.14a4bf7b6b7d40dc",
|
||||
"source": "Skip",
|
||||
"translated": "Пропустить"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6c1adc16e0a846e5",
|
||||
@@ -23064,14 +23099,9 @@
|
||||
"translated": "Отправка…"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.698bdb0d6537edb6",
|
||||
"source": "Expires in \\(seconds / 60)m \\(seconds % 60)s",
|
||||
"translated": "Истекает через \\(seconds / 60) мин \\(seconds % 60) с"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.30e1c342b3345290",
|
||||
"source": "Expires in \\(seconds)s",
|
||||
"translated": "Истекает через \\(seconds) с"
|
||||
"id": "native.apple.e3095b651fc46b52",
|
||||
"source": "Submit",
|
||||
"translated": "Отправить"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7cd6fb01b9f5d14a",
|
||||
|
||||
+71
-41
@@ -7388,11 +7388,36 @@
|
||||
"source": "Other answer",
|
||||
"translated": "Annat svar"
|
||||
},
|
||||
{
|
||||
"id": "native.android.de591e6e2e440ecb",
|
||||
"source": "Skip",
|
||||
"translated": "Hoppa över"
|
||||
},
|
||||
{
|
||||
"id": "native.android.92dfa7979d04989c",
|
||||
"source": "Submitting…",
|
||||
"translated": "Skickar…"
|
||||
},
|
||||
{
|
||||
"id": "native.android.fa81739f52e7697f",
|
||||
"source": "Submit",
|
||||
"translated": "Skicka"
|
||||
},
|
||||
{
|
||||
"id": "native.android.7da074848cacfe4b",
|
||||
"source": "Skipped",
|
||||
"translated": "Överhoppad"
|
||||
},
|
||||
{
|
||||
"id": "native.android.1a0e6b33059a63aa",
|
||||
"source": "Expired",
|
||||
"translated": "Utgången"
|
||||
},
|
||||
{
|
||||
"id": "native.android.86c8d6113d07ef82",
|
||||
"source": "Unavailable",
|
||||
"translated": "Inte tillgänglig"
|
||||
},
|
||||
{
|
||||
"id": "native.android.f68cd7a84be4089e",
|
||||
"source": "Answered",
|
||||
@@ -7403,36 +7428,6 @@
|
||||
"source": "Answered elsewhere",
|
||||
"translated": "Besvarad på annat håll"
|
||||
},
|
||||
{
|
||||
"id": "native.android.1a0e6b33059a63aa",
|
||||
"source": "Expired",
|
||||
"translated": "Utgången"
|
||||
},
|
||||
{
|
||||
"id": "native.android.d6d8fc83cec68418",
|
||||
"source": "Cancelled",
|
||||
"translated": "Avbruten"
|
||||
},
|
||||
{
|
||||
"id": "native.android.73f64d0d54b7f9c0",
|
||||
"source": "Pending",
|
||||
"translated": "Väntande"
|
||||
},
|
||||
{
|
||||
"id": "native.android.92dfa7979d04989c",
|
||||
"source": "Submitting…",
|
||||
"translated": "Skickar…"
|
||||
},
|
||||
{
|
||||
"id": "native.android.c5e9ee99f9d9c612",
|
||||
"source": "Expires in ${seconds / 60}m ${seconds % 60}s",
|
||||
"translated": "Upphör om ${seconds / 60}m ${seconds % 60}s"
|
||||
},
|
||||
{
|
||||
"id": "native.android.2ca602fbdf96f1e7",
|
||||
"source": "Expires in ${seconds}s",
|
||||
"translated": "Upphör om ${seconds}s"
|
||||
},
|
||||
{
|
||||
"id": "native.android.118e8d239b18895e",
|
||||
"source": "Model",
|
||||
@@ -23038,6 +23033,41 @@
|
||||
"source": "Expand plan",
|
||||
"translated": "Visa plan"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.ce899320a84d4086",
|
||||
"source": "Answered",
|
||||
"translated": "Besvarad"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f019b6a07d5a33d8",
|
||||
"source": "Answered elsewhere",
|
||||
"translated": "Besvarad på annat håll"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.ebdedeffad570c43",
|
||||
"source": "Skipped",
|
||||
"translated": "Överhoppad"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.8dde2eaaf6b0dfa9",
|
||||
"source": "Expired",
|
||||
"translated": "Utgången"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6dff9e9194347f3b",
|
||||
"source": "Unavailable",
|
||||
"translated": "Inte tillgänglig"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b34cd890366824f0",
|
||||
"source": "Pending",
|
||||
"translated": "Väntande"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b97da55b7e16e60c",
|
||||
"source": "Question summary",
|
||||
"translated": "Frågesammanfattning"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.da4494f9e875169d",
|
||||
"source": "Other answer",
|
||||
@@ -23054,9 +23084,14 @@
|
||||
"translated": "Valt"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.e3095b651fc46b52",
|
||||
"source": "Submit",
|
||||
"translated": "Skicka"
|
||||
"id": "native.apple.1bbbbfd1ad3842ce",
|
||||
"source": "Skipping…",
|
||||
"translated": "Hoppar över…"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.14a4bf7b6b7d40dc",
|
||||
"source": "Skip",
|
||||
"translated": "Hoppa över"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6c1adc16e0a846e5",
|
||||
@@ -23064,14 +23099,9 @@
|
||||
"translated": "Skickar…"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.698bdb0d6537edb6",
|
||||
"source": "Expires in \\(seconds / 60)m \\(seconds % 60)s",
|
||||
"translated": "Upphör om \\(seconds / 60)m \\(seconds % 60)s"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.30e1c342b3345290",
|
||||
"source": "Expires in \\(seconds)s",
|
||||
"translated": "Upphör om \\(seconds)s"
|
||||
"id": "native.apple.e3095b651fc46b52",
|
||||
"source": "Submit",
|
||||
"translated": "Skicka"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7cd6fb01b9f5d14a",
|
||||
|
||||
+71
-41
@@ -7388,11 +7388,36 @@
|
||||
"source": "Other answer",
|
||||
"translated": "คำตอบอื่น"
|
||||
},
|
||||
{
|
||||
"id": "native.android.de591e6e2e440ecb",
|
||||
"source": "Skip",
|
||||
"translated": "ข้าม"
|
||||
},
|
||||
{
|
||||
"id": "native.android.92dfa7979d04989c",
|
||||
"source": "Submitting…",
|
||||
"translated": "กำลังส่ง…"
|
||||
},
|
||||
{
|
||||
"id": "native.android.fa81739f52e7697f",
|
||||
"source": "Submit",
|
||||
"translated": "ส่ง"
|
||||
},
|
||||
{
|
||||
"id": "native.android.7da074848cacfe4b",
|
||||
"source": "Skipped",
|
||||
"translated": "ข้ามแล้ว"
|
||||
},
|
||||
{
|
||||
"id": "native.android.1a0e6b33059a63aa",
|
||||
"source": "Expired",
|
||||
"translated": "หมดอายุแล้ว"
|
||||
},
|
||||
{
|
||||
"id": "native.android.86c8d6113d07ef82",
|
||||
"source": "Unavailable",
|
||||
"translated": "ไม่พร้อมใช้งาน"
|
||||
},
|
||||
{
|
||||
"id": "native.android.f68cd7a84be4089e",
|
||||
"source": "Answered",
|
||||
@@ -7403,36 +7428,6 @@
|
||||
"source": "Answered elsewhere",
|
||||
"translated": "ตอบจากที่อื่นแล้ว"
|
||||
},
|
||||
{
|
||||
"id": "native.android.1a0e6b33059a63aa",
|
||||
"source": "Expired",
|
||||
"translated": "หมดอายุแล้ว"
|
||||
},
|
||||
{
|
||||
"id": "native.android.d6d8fc83cec68418",
|
||||
"source": "Cancelled",
|
||||
"translated": "ยกเลิกแล้ว"
|
||||
},
|
||||
{
|
||||
"id": "native.android.73f64d0d54b7f9c0",
|
||||
"source": "Pending",
|
||||
"translated": "รอดำเนินการ"
|
||||
},
|
||||
{
|
||||
"id": "native.android.92dfa7979d04989c",
|
||||
"source": "Submitting…",
|
||||
"translated": "กำลังส่ง…"
|
||||
},
|
||||
{
|
||||
"id": "native.android.c5e9ee99f9d9c612",
|
||||
"source": "Expires in ${seconds / 60}m ${seconds % 60}s",
|
||||
"translated": "หมดอายุใน ${seconds / 60} นาที ${seconds % 60} วินาที"
|
||||
},
|
||||
{
|
||||
"id": "native.android.2ca602fbdf96f1e7",
|
||||
"source": "Expires in ${seconds}s",
|
||||
"translated": "หมดอายุใน ${seconds} วินาที"
|
||||
},
|
||||
{
|
||||
"id": "native.android.118e8d239b18895e",
|
||||
"source": "Model",
|
||||
@@ -23038,6 +23033,41 @@
|
||||
"source": "Expand plan",
|
||||
"translated": "ขยายแผน"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.ce899320a84d4086",
|
||||
"source": "Answered",
|
||||
"translated": "ตอบแล้ว"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f019b6a07d5a33d8",
|
||||
"source": "Answered elsewhere",
|
||||
"translated": "ตอบจากที่อื่นแล้ว"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.ebdedeffad570c43",
|
||||
"source": "Skipped",
|
||||
"translated": "ข้ามแล้ว"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.8dde2eaaf6b0dfa9",
|
||||
"source": "Expired",
|
||||
"translated": "หมดอายุแล้ว"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6dff9e9194347f3b",
|
||||
"source": "Unavailable",
|
||||
"translated": "ไม่พร้อมใช้งาน"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b34cd890366824f0",
|
||||
"source": "Pending",
|
||||
"translated": "รอดำเนินการ"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b97da55b7e16e60c",
|
||||
"source": "Question summary",
|
||||
"translated": "สรุปคำถาม"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.da4494f9e875169d",
|
||||
"source": "Other answer",
|
||||
@@ -23054,9 +23084,14 @@
|
||||
"translated": "เลือกแล้ว"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.e3095b651fc46b52",
|
||||
"source": "Submit",
|
||||
"translated": "ส่ง"
|
||||
"id": "native.apple.1bbbbfd1ad3842ce",
|
||||
"source": "Skipping…",
|
||||
"translated": "กำลังข้าม…"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.14a4bf7b6b7d40dc",
|
||||
"source": "Skip",
|
||||
"translated": "ข้าม"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6c1adc16e0a846e5",
|
||||
@@ -23064,14 +23099,9 @@
|
||||
"translated": "กำลังส่ง…"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.698bdb0d6537edb6",
|
||||
"source": "Expires in \\(seconds / 60)m \\(seconds % 60)s",
|
||||
"translated": "หมดอายุใน \\(seconds / 60) นาที \\(seconds % 60) วินาที"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.30e1c342b3345290",
|
||||
"source": "Expires in \\(seconds)s",
|
||||
"translated": "หมดอายุใน \\(seconds) วินาที"
|
||||
"id": "native.apple.e3095b651fc46b52",
|
||||
"source": "Submit",
|
||||
"translated": "ส่ง"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7cd6fb01b9f5d14a",
|
||||
|
||||
+71
-41
@@ -7388,11 +7388,36 @@
|
||||
"source": "Other answer",
|
||||
"translated": "Diğer yanıt"
|
||||
},
|
||||
{
|
||||
"id": "native.android.de591e6e2e440ecb",
|
||||
"source": "Skip",
|
||||
"translated": "Atla"
|
||||
},
|
||||
{
|
||||
"id": "native.android.92dfa7979d04989c",
|
||||
"source": "Submitting…",
|
||||
"translated": "Gönderiliyor…"
|
||||
},
|
||||
{
|
||||
"id": "native.android.fa81739f52e7697f",
|
||||
"source": "Submit",
|
||||
"translated": "Gönder"
|
||||
},
|
||||
{
|
||||
"id": "native.android.7da074848cacfe4b",
|
||||
"source": "Skipped",
|
||||
"translated": "Atlandı"
|
||||
},
|
||||
{
|
||||
"id": "native.android.1a0e6b33059a63aa",
|
||||
"source": "Expired",
|
||||
"translated": "Süresi doldu"
|
||||
},
|
||||
{
|
||||
"id": "native.android.86c8d6113d07ef82",
|
||||
"source": "Unavailable",
|
||||
"translated": "Kullanılamıyor"
|
||||
},
|
||||
{
|
||||
"id": "native.android.f68cd7a84be4089e",
|
||||
"source": "Answered",
|
||||
@@ -7403,36 +7428,6 @@
|
||||
"source": "Answered elsewhere",
|
||||
"translated": "Başka yerde yanıtlandı"
|
||||
},
|
||||
{
|
||||
"id": "native.android.1a0e6b33059a63aa",
|
||||
"source": "Expired",
|
||||
"translated": "Süresi doldu"
|
||||
},
|
||||
{
|
||||
"id": "native.android.d6d8fc83cec68418",
|
||||
"source": "Cancelled",
|
||||
"translated": "İptal edildi"
|
||||
},
|
||||
{
|
||||
"id": "native.android.73f64d0d54b7f9c0",
|
||||
"source": "Pending",
|
||||
"translated": "Bekliyor"
|
||||
},
|
||||
{
|
||||
"id": "native.android.92dfa7979d04989c",
|
||||
"source": "Submitting…",
|
||||
"translated": "Gönderiliyor…"
|
||||
},
|
||||
{
|
||||
"id": "native.android.c5e9ee99f9d9c612",
|
||||
"source": "Expires in ${seconds / 60}m ${seconds % 60}s",
|
||||
"translated": "Sona ermesine ${seconds / 60} dk ${seconds % 60} sn"
|
||||
},
|
||||
{
|
||||
"id": "native.android.2ca602fbdf96f1e7",
|
||||
"source": "Expires in ${seconds}s",
|
||||
"translated": "Sona ermesine ${seconds} sn"
|
||||
},
|
||||
{
|
||||
"id": "native.android.118e8d239b18895e",
|
||||
"source": "Model",
|
||||
@@ -23038,6 +23033,41 @@
|
||||
"source": "Expand plan",
|
||||
"translated": "Planı genişlet"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.ce899320a84d4086",
|
||||
"source": "Answered",
|
||||
"translated": "Yanıtlandı"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f019b6a07d5a33d8",
|
||||
"source": "Answered elsewhere",
|
||||
"translated": "Başka yerde yanıtlandı"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.ebdedeffad570c43",
|
||||
"source": "Skipped",
|
||||
"translated": "Atlandı"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.8dde2eaaf6b0dfa9",
|
||||
"source": "Expired",
|
||||
"translated": "Süresi doldu"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6dff9e9194347f3b",
|
||||
"source": "Unavailable",
|
||||
"translated": "Kullanılamıyor"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b34cd890366824f0",
|
||||
"source": "Pending",
|
||||
"translated": "Beklemede"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b97da55b7e16e60c",
|
||||
"source": "Question summary",
|
||||
"translated": "Soru özeti"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.da4494f9e875169d",
|
||||
"source": "Other answer",
|
||||
@@ -23054,9 +23084,14 @@
|
||||
"translated": "Seçildi"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.e3095b651fc46b52",
|
||||
"source": "Submit",
|
||||
"translated": "Gönder"
|
||||
"id": "native.apple.1bbbbfd1ad3842ce",
|
||||
"source": "Skipping…",
|
||||
"translated": "Atlanıyor…"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.14a4bf7b6b7d40dc",
|
||||
"source": "Skip",
|
||||
"translated": "Atla"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6c1adc16e0a846e5",
|
||||
@@ -23064,14 +23099,9 @@
|
||||
"translated": "Gönderiliyor…"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.698bdb0d6537edb6",
|
||||
"source": "Expires in \\(seconds / 60)m \\(seconds % 60)s",
|
||||
"translated": "Sona ermesine \\(seconds / 60) dk \\(seconds % 60) sn"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.30e1c342b3345290",
|
||||
"source": "Expires in \\(seconds)s",
|
||||
"translated": "Sona ermesine \\(seconds) sn"
|
||||
"id": "native.apple.e3095b651fc46b52",
|
||||
"source": "Submit",
|
||||
"translated": "Gönder"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7cd6fb01b9f5d14a",
|
||||
|
||||
+71
-41
@@ -7388,11 +7388,36 @@
|
||||
"source": "Other answer",
|
||||
"translated": "Інша відповідь"
|
||||
},
|
||||
{
|
||||
"id": "native.android.de591e6e2e440ecb",
|
||||
"source": "Skip",
|
||||
"translated": "Пропустити"
|
||||
},
|
||||
{
|
||||
"id": "native.android.92dfa7979d04989c",
|
||||
"source": "Submitting…",
|
||||
"translated": "Надсилання…"
|
||||
},
|
||||
{
|
||||
"id": "native.android.fa81739f52e7697f",
|
||||
"source": "Submit",
|
||||
"translated": "Надіслати"
|
||||
},
|
||||
{
|
||||
"id": "native.android.7da074848cacfe4b",
|
||||
"source": "Skipped",
|
||||
"translated": "Пропущено"
|
||||
},
|
||||
{
|
||||
"id": "native.android.1a0e6b33059a63aa",
|
||||
"source": "Expired",
|
||||
"translated": "Термін дії минув"
|
||||
},
|
||||
{
|
||||
"id": "native.android.86c8d6113d07ef82",
|
||||
"source": "Unavailable",
|
||||
"translated": "Недоступно"
|
||||
},
|
||||
{
|
||||
"id": "native.android.f68cd7a84be4089e",
|
||||
"source": "Answered",
|
||||
@@ -7403,36 +7428,6 @@
|
||||
"source": "Answered elsewhere",
|
||||
"translated": "Відповідь надано в іншому місці"
|
||||
},
|
||||
{
|
||||
"id": "native.android.1a0e6b33059a63aa",
|
||||
"source": "Expired",
|
||||
"translated": "Термін дії минув"
|
||||
},
|
||||
{
|
||||
"id": "native.android.d6d8fc83cec68418",
|
||||
"source": "Cancelled",
|
||||
"translated": "Скасовано"
|
||||
},
|
||||
{
|
||||
"id": "native.android.73f64d0d54b7f9c0",
|
||||
"source": "Pending",
|
||||
"translated": "Очікує"
|
||||
},
|
||||
{
|
||||
"id": "native.android.92dfa7979d04989c",
|
||||
"source": "Submitting…",
|
||||
"translated": "Надсилання…"
|
||||
},
|
||||
{
|
||||
"id": "native.android.c5e9ee99f9d9c612",
|
||||
"source": "Expires in ${seconds / 60}m ${seconds % 60}s",
|
||||
"translated": "Залишилося ${seconds / 60} хв ${seconds % 60} с"
|
||||
},
|
||||
{
|
||||
"id": "native.android.2ca602fbdf96f1e7",
|
||||
"source": "Expires in ${seconds}s",
|
||||
"translated": "Залишилося ${seconds} с"
|
||||
},
|
||||
{
|
||||
"id": "native.android.118e8d239b18895e",
|
||||
"source": "Model",
|
||||
@@ -23038,6 +23033,41 @@
|
||||
"source": "Expand plan",
|
||||
"translated": "Розгорнути план"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.ce899320a84d4086",
|
||||
"source": "Answered",
|
||||
"translated": "Відповідь надано"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f019b6a07d5a33d8",
|
||||
"source": "Answered elsewhere",
|
||||
"translated": "Відповідь надано в іншому місці"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.ebdedeffad570c43",
|
||||
"source": "Skipped",
|
||||
"translated": "Пропущено"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.8dde2eaaf6b0dfa9",
|
||||
"source": "Expired",
|
||||
"translated": "Термін дії минув"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6dff9e9194347f3b",
|
||||
"source": "Unavailable",
|
||||
"translated": "Недоступно"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b34cd890366824f0",
|
||||
"source": "Pending",
|
||||
"translated": "Очікує"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b97da55b7e16e60c",
|
||||
"source": "Question summary",
|
||||
"translated": "Підсумок запитання"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.da4494f9e875169d",
|
||||
"source": "Other answer",
|
||||
@@ -23054,9 +23084,14 @@
|
||||
"translated": "Вибрано"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.e3095b651fc46b52",
|
||||
"source": "Submit",
|
||||
"translated": "Надіслати"
|
||||
"id": "native.apple.1bbbbfd1ad3842ce",
|
||||
"source": "Skipping…",
|
||||
"translated": "Пропуск…"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.14a4bf7b6b7d40dc",
|
||||
"source": "Skip",
|
||||
"translated": "Пропустити"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6c1adc16e0a846e5",
|
||||
@@ -23064,14 +23099,9 @@
|
||||
"translated": "Надсилання…"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.698bdb0d6537edb6",
|
||||
"source": "Expires in \\(seconds / 60)m \\(seconds % 60)s",
|
||||
"translated": "Залишилося \\(seconds / 60) хв \\(seconds % 60) с"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.30e1c342b3345290",
|
||||
"source": "Expires in \\(seconds)s",
|
||||
"translated": "Залишилося \\(seconds) с"
|
||||
"id": "native.apple.e3095b651fc46b52",
|
||||
"source": "Submit",
|
||||
"translated": "Надіслати"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7cd6fb01b9f5d14a",
|
||||
|
||||
+71
-41
@@ -7388,11 +7388,36 @@
|
||||
"source": "Other answer",
|
||||
"translated": "Câu trả lời khác"
|
||||
},
|
||||
{
|
||||
"id": "native.android.de591e6e2e440ecb",
|
||||
"source": "Skip",
|
||||
"translated": "Bỏ qua"
|
||||
},
|
||||
{
|
||||
"id": "native.android.92dfa7979d04989c",
|
||||
"source": "Submitting…",
|
||||
"translated": "Đang gửi…"
|
||||
},
|
||||
{
|
||||
"id": "native.android.fa81739f52e7697f",
|
||||
"source": "Submit",
|
||||
"translated": "Gửi"
|
||||
},
|
||||
{
|
||||
"id": "native.android.7da074848cacfe4b",
|
||||
"source": "Skipped",
|
||||
"translated": "Đã bỏ qua"
|
||||
},
|
||||
{
|
||||
"id": "native.android.1a0e6b33059a63aa",
|
||||
"source": "Expired",
|
||||
"translated": "Đã hết hạn"
|
||||
},
|
||||
{
|
||||
"id": "native.android.86c8d6113d07ef82",
|
||||
"source": "Unavailable",
|
||||
"translated": "Không khả dụng"
|
||||
},
|
||||
{
|
||||
"id": "native.android.f68cd7a84be4089e",
|
||||
"source": "Answered",
|
||||
@@ -7403,36 +7428,6 @@
|
||||
"source": "Answered elsewhere",
|
||||
"translated": "Đã trả lời ở nơi khác"
|
||||
},
|
||||
{
|
||||
"id": "native.android.1a0e6b33059a63aa",
|
||||
"source": "Expired",
|
||||
"translated": "Đã hết hạn"
|
||||
},
|
||||
{
|
||||
"id": "native.android.d6d8fc83cec68418",
|
||||
"source": "Cancelled",
|
||||
"translated": "Đã hủy"
|
||||
},
|
||||
{
|
||||
"id": "native.android.73f64d0d54b7f9c0",
|
||||
"source": "Pending",
|
||||
"translated": "Đang chờ"
|
||||
},
|
||||
{
|
||||
"id": "native.android.92dfa7979d04989c",
|
||||
"source": "Submitting…",
|
||||
"translated": "Đang gửi…"
|
||||
},
|
||||
{
|
||||
"id": "native.android.c5e9ee99f9d9c612",
|
||||
"source": "Expires in ${seconds / 60}m ${seconds % 60}s",
|
||||
"translated": "Hết hạn sau ${seconds / 60}m ${seconds % 60}s"
|
||||
},
|
||||
{
|
||||
"id": "native.android.2ca602fbdf96f1e7",
|
||||
"source": "Expires in ${seconds}s",
|
||||
"translated": "Hết hạn sau ${seconds}s"
|
||||
},
|
||||
{
|
||||
"id": "native.android.118e8d239b18895e",
|
||||
"source": "Model",
|
||||
@@ -23038,6 +23033,41 @@
|
||||
"source": "Expand plan",
|
||||
"translated": "Mở rộng kế hoạch"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.ce899320a84d4086",
|
||||
"source": "Answered",
|
||||
"translated": "Đã trả lời"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f019b6a07d5a33d8",
|
||||
"source": "Answered elsewhere",
|
||||
"translated": "Đã trả lời ở nơi khác"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.ebdedeffad570c43",
|
||||
"source": "Skipped",
|
||||
"translated": "Đã bỏ qua"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.8dde2eaaf6b0dfa9",
|
||||
"source": "Expired",
|
||||
"translated": "Đã hết hạn"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6dff9e9194347f3b",
|
||||
"source": "Unavailable",
|
||||
"translated": "Không khả dụng"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b34cd890366824f0",
|
||||
"source": "Pending",
|
||||
"translated": "Đang chờ"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b97da55b7e16e60c",
|
||||
"source": "Question summary",
|
||||
"translated": "Tóm tắt câu hỏi"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.da4494f9e875169d",
|
||||
"source": "Other answer",
|
||||
@@ -23054,9 +23084,14 @@
|
||||
"translated": "Đã chọn"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.e3095b651fc46b52",
|
||||
"source": "Submit",
|
||||
"translated": "Gửi"
|
||||
"id": "native.apple.1bbbbfd1ad3842ce",
|
||||
"source": "Skipping…",
|
||||
"translated": "Đang bỏ qua…"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.14a4bf7b6b7d40dc",
|
||||
"source": "Skip",
|
||||
"translated": "Bỏ qua"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6c1adc16e0a846e5",
|
||||
@@ -23064,14 +23099,9 @@
|
||||
"translated": "Đang gửi…"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.698bdb0d6537edb6",
|
||||
"source": "Expires in \\(seconds / 60)m \\(seconds % 60)s",
|
||||
"translated": "Hết hạn sau \\(seconds / 60)m \\(seconds % 60)s"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.30e1c342b3345290",
|
||||
"source": "Expires in \\(seconds)s",
|
||||
"translated": "Hết hạn sau \\(seconds)s"
|
||||
"id": "native.apple.e3095b651fc46b52",
|
||||
"source": "Submit",
|
||||
"translated": "Gửi"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7cd6fb01b9f5d14a",
|
||||
|
||||
@@ -7388,11 +7388,36 @@
|
||||
"source": "Other answer",
|
||||
"translated": "其他答案"
|
||||
},
|
||||
{
|
||||
"id": "native.android.de591e6e2e440ecb",
|
||||
"source": "Skip",
|
||||
"translated": "跳过"
|
||||
},
|
||||
{
|
||||
"id": "native.android.92dfa7979d04989c",
|
||||
"source": "Submitting…",
|
||||
"translated": "正在提交…"
|
||||
},
|
||||
{
|
||||
"id": "native.android.fa81739f52e7697f",
|
||||
"source": "Submit",
|
||||
"translated": "提交"
|
||||
},
|
||||
{
|
||||
"id": "native.android.7da074848cacfe4b",
|
||||
"source": "Skipped",
|
||||
"translated": "已跳过"
|
||||
},
|
||||
{
|
||||
"id": "native.android.1a0e6b33059a63aa",
|
||||
"source": "Expired",
|
||||
"translated": "已过期"
|
||||
},
|
||||
{
|
||||
"id": "native.android.86c8d6113d07ef82",
|
||||
"source": "Unavailable",
|
||||
"translated": "不可用"
|
||||
},
|
||||
{
|
||||
"id": "native.android.f68cd7a84be4089e",
|
||||
"source": "Answered",
|
||||
@@ -7403,36 +7428,6 @@
|
||||
"source": "Answered elsewhere",
|
||||
"translated": "已在其他地方回答"
|
||||
},
|
||||
{
|
||||
"id": "native.android.1a0e6b33059a63aa",
|
||||
"source": "Expired",
|
||||
"translated": "已过期"
|
||||
},
|
||||
{
|
||||
"id": "native.android.d6d8fc83cec68418",
|
||||
"source": "Cancelled",
|
||||
"translated": "已取消"
|
||||
},
|
||||
{
|
||||
"id": "native.android.73f64d0d54b7f9c0",
|
||||
"source": "Pending",
|
||||
"translated": "待处理"
|
||||
},
|
||||
{
|
||||
"id": "native.android.92dfa7979d04989c",
|
||||
"source": "Submitting…",
|
||||
"translated": "正在提交…"
|
||||
},
|
||||
{
|
||||
"id": "native.android.c5e9ee99f9d9c612",
|
||||
"source": "Expires in ${seconds / 60}m ${seconds % 60}s",
|
||||
"translated": "${seconds / 60}分${seconds % 60}秒后过期"
|
||||
},
|
||||
{
|
||||
"id": "native.android.2ca602fbdf96f1e7",
|
||||
"source": "Expires in ${seconds}s",
|
||||
"translated": "${seconds}秒后过期"
|
||||
},
|
||||
{
|
||||
"id": "native.android.118e8d239b18895e",
|
||||
"source": "Model",
|
||||
@@ -23038,6 +23033,41 @@
|
||||
"source": "Expand plan",
|
||||
"translated": "展开计划"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.ce899320a84d4086",
|
||||
"source": "Answered",
|
||||
"translated": "已回答"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f019b6a07d5a33d8",
|
||||
"source": "Answered elsewhere",
|
||||
"translated": "已在其他地方回答"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.ebdedeffad570c43",
|
||||
"source": "Skipped",
|
||||
"translated": "已跳过"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.8dde2eaaf6b0dfa9",
|
||||
"source": "Expired",
|
||||
"translated": "已过期"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6dff9e9194347f3b",
|
||||
"source": "Unavailable",
|
||||
"translated": "不可用"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b34cd890366824f0",
|
||||
"source": "Pending",
|
||||
"translated": "待处理"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b97da55b7e16e60c",
|
||||
"source": "Question summary",
|
||||
"translated": "问题摘要"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.da4494f9e875169d",
|
||||
"source": "Other answer",
|
||||
@@ -23054,9 +23084,14 @@
|
||||
"translated": "已选择"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.e3095b651fc46b52",
|
||||
"source": "Submit",
|
||||
"translated": "提交"
|
||||
"id": "native.apple.1bbbbfd1ad3842ce",
|
||||
"source": "Skipping…",
|
||||
"translated": "正在跳过…"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.14a4bf7b6b7d40dc",
|
||||
"source": "Skip",
|
||||
"translated": "跳过"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6c1adc16e0a846e5",
|
||||
@@ -23064,14 +23099,9 @@
|
||||
"translated": "正在提交…"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.698bdb0d6537edb6",
|
||||
"source": "Expires in \\(seconds / 60)m \\(seconds % 60)s",
|
||||
"translated": "\\(seconds / 60)分\\(seconds % 60)秒后过期"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.30e1c342b3345290",
|
||||
"source": "Expires in \\(seconds)s",
|
||||
"translated": "\\(seconds)秒后过期"
|
||||
"id": "native.apple.e3095b651fc46b52",
|
||||
"source": "Submit",
|
||||
"translated": "提交"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7cd6fb01b9f5d14a",
|
||||
|
||||
@@ -7388,11 +7388,36 @@
|
||||
"source": "Other answer",
|
||||
"translated": "其他答案"
|
||||
},
|
||||
{
|
||||
"id": "native.android.de591e6e2e440ecb",
|
||||
"source": "Skip",
|
||||
"translated": "略過"
|
||||
},
|
||||
{
|
||||
"id": "native.android.92dfa7979d04989c",
|
||||
"source": "Submitting…",
|
||||
"translated": "正在提交…"
|
||||
},
|
||||
{
|
||||
"id": "native.android.fa81739f52e7697f",
|
||||
"source": "Submit",
|
||||
"translated": "提交"
|
||||
},
|
||||
{
|
||||
"id": "native.android.7da074848cacfe4b",
|
||||
"source": "Skipped",
|
||||
"translated": "已略過"
|
||||
},
|
||||
{
|
||||
"id": "native.android.1a0e6b33059a63aa",
|
||||
"source": "Expired",
|
||||
"translated": "已過期"
|
||||
},
|
||||
{
|
||||
"id": "native.android.86c8d6113d07ef82",
|
||||
"source": "Unavailable",
|
||||
"translated": "無法使用"
|
||||
},
|
||||
{
|
||||
"id": "native.android.f68cd7a84be4089e",
|
||||
"source": "Answered",
|
||||
@@ -7403,36 +7428,6 @@
|
||||
"source": "Answered elsewhere",
|
||||
"translated": "已在其他地方回答"
|
||||
},
|
||||
{
|
||||
"id": "native.android.1a0e6b33059a63aa",
|
||||
"source": "Expired",
|
||||
"translated": "已過期"
|
||||
},
|
||||
{
|
||||
"id": "native.android.d6d8fc83cec68418",
|
||||
"source": "Cancelled",
|
||||
"translated": "已取消"
|
||||
},
|
||||
{
|
||||
"id": "native.android.73f64d0d54b7f9c0",
|
||||
"source": "Pending",
|
||||
"translated": "待處理"
|
||||
},
|
||||
{
|
||||
"id": "native.android.92dfa7979d04989c",
|
||||
"source": "Submitting…",
|
||||
"translated": "正在提交…"
|
||||
},
|
||||
{
|
||||
"id": "native.android.c5e9ee99f9d9c612",
|
||||
"source": "Expires in ${seconds / 60}m ${seconds % 60}s",
|
||||
"translated": "將於 ${seconds / 60}m ${seconds % 60}s 後過期"
|
||||
},
|
||||
{
|
||||
"id": "native.android.2ca602fbdf96f1e7",
|
||||
"source": "Expires in ${seconds}s",
|
||||
"translated": "將於 ${seconds}s 後過期"
|
||||
},
|
||||
{
|
||||
"id": "native.android.118e8d239b18895e",
|
||||
"source": "Model",
|
||||
@@ -23038,6 +23033,41 @@
|
||||
"source": "Expand plan",
|
||||
"translated": "展開計畫"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.ce899320a84d4086",
|
||||
"source": "Answered",
|
||||
"translated": "已回答"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f019b6a07d5a33d8",
|
||||
"source": "Answered elsewhere",
|
||||
"translated": "已在其他地方回答"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.ebdedeffad570c43",
|
||||
"source": "Skipped",
|
||||
"translated": "已略過"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.8dde2eaaf6b0dfa9",
|
||||
"source": "Expired",
|
||||
"translated": "已過期"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6dff9e9194347f3b",
|
||||
"source": "Unavailable",
|
||||
"translated": "無法使用"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b34cd890366824f0",
|
||||
"source": "Pending",
|
||||
"translated": "待處理"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.b97da55b7e16e60c",
|
||||
"source": "Question summary",
|
||||
"translated": "問題摘要"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.da4494f9e875169d",
|
||||
"source": "Other answer",
|
||||
@@ -23054,9 +23084,14 @@
|
||||
"translated": "已選取"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.e3095b651fc46b52",
|
||||
"source": "Submit",
|
||||
"translated": "提交"
|
||||
"id": "native.apple.1bbbbfd1ad3842ce",
|
||||
"source": "Skipping…",
|
||||
"translated": "正在略過…"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.14a4bf7b6b7d40dc",
|
||||
"source": "Skip",
|
||||
"translated": "略過"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6c1adc16e0a846e5",
|
||||
@@ -23064,14 +23099,9 @@
|
||||
"translated": "正在提交…"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.698bdb0d6537edb6",
|
||||
"source": "Expires in \\(seconds / 60)m \\(seconds % 60)s",
|
||||
"translated": "將於 \\(seconds / 60)m \\(seconds % 60)s 後過期"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.30e1c342b3345290",
|
||||
"source": "Expires in \\(seconds)s",
|
||||
"translated": "將於 \\(seconds)s 後過期"
|
||||
"id": "native.apple.e3095b651fc46b52",
|
||||
"source": "Submit",
|
||||
"translated": "提交"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7cd6fb01b9f5d14a",
|
||||
|
||||
@@ -1667,6 +1667,10 @@ class MainViewModel private constructor(
|
||||
ensureRuntime().resolveChatQuestion(id, answers)
|
||||
}
|
||||
|
||||
fun skipChatQuestion(id: String) {
|
||||
ensureRuntime().skipChatQuestion(id)
|
||||
}
|
||||
|
||||
suspend fun listBackgroundTasks(agentId: String): List<BackgroundTask> = ensureRuntime().listBackgroundTasks(agentId)
|
||||
|
||||
suspend fun getBackgroundTask(taskId: String): BackgroundTask = ensureRuntime().getBackgroundTask(taskId)
|
||||
|
||||
@@ -2434,6 +2434,8 @@ class NodeRuntime private constructor(
|
||||
answers: Map<String, List<String>>,
|
||||
) = chat.resolveQuestion(id, answers)
|
||||
|
||||
fun skipChatQuestion(id: String) = chat.skipQuestion(id)
|
||||
|
||||
private fun applyScreenshotFixture() {
|
||||
check(BuildConfig.DEBUG) { "Android screenshot fixtures require a debug build" }
|
||||
_serverName.value = "OpenClaw Gateway"
|
||||
|
||||
@@ -6,6 +6,9 @@ import ai.openclaw.app.gateway.GatewayRequestNotEnqueued
|
||||
import ai.openclaw.app.gateway.GatewayRequestOutcomeUnknown
|
||||
import ai.openclaw.app.gateway.GatewayRequestRejected
|
||||
import ai.openclaw.app.gateway.GatewaySession
|
||||
import ai.openclaw.app.gateway.QuestionAnswers
|
||||
import ai.openclaw.app.gateway.QuestionAnswersAnswersValue
|
||||
import ai.openclaw.app.gateway.QuestionGetResult
|
||||
import ai.openclaw.app.gateway.QuestionListResult
|
||||
import ai.openclaw.app.gateway.QuestionRecord
|
||||
import ai.openclaw.app.gateway.parseChatSendAck
|
||||
@@ -37,6 +40,7 @@ import kotlinx.serialization.json.JsonNull
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.decodeFromJsonElement
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import java.util.Base64
|
||||
import java.util.Locale
|
||||
@@ -47,6 +51,7 @@ import java.util.concurrent.atomic.AtomicLong
|
||||
|
||||
// Bounds one-shot search list fetches like the primary session list.
|
||||
internal const val SESSION_LIST_FETCH_LIMIT = 200
|
||||
private val QUESTION_REFRESH_RETRY_DELAYS_MS = longArrayOf(1_000L, 2_000L, 4_000L)
|
||||
|
||||
internal fun chatOutboxQueueFailureText(): NativeText = ChatController.queueFailureText()
|
||||
|
||||
@@ -2100,40 +2105,70 @@ class ChatController internal constructor(
|
||||
fun resolveQuestion(
|
||||
id: String,
|
||||
answers: Map<String, List<String>>,
|
||||
) = resolveQuestion(id = id, answers = answers, cancel = false)
|
||||
|
||||
fun skipQuestion(id: String) = resolveQuestion(id = id, answers = null, cancel = true)
|
||||
|
||||
private fun resolveQuestion(
|
||||
id: String,
|
||||
answers: Map<String, List<String>>?,
|
||||
cancel: Boolean,
|
||||
) {
|
||||
val gatewayId = currentCacheScope()?.gatewayId
|
||||
var claimed = false
|
||||
updateQuestions { prompts ->
|
||||
prompts.map { prompt ->
|
||||
if (prompt.record.id == id) prompt.copy(submitting = true, errorText = null) else prompt
|
||||
if (prompt.record.id == id && prompt.status() == ChatQuestionStatus.Pending) {
|
||||
claimed = true
|
||||
prompt.copy(submitting = true, skipping = cancel, errorText = null)
|
||||
} else {
|
||||
prompt
|
||||
}
|
||||
}
|
||||
}
|
||||
// updateQuestions owns the question-state lock, so competing answer/skip callbacks
|
||||
// observe the first claim as Submitting and cannot launch a second mutation.
|
||||
if (!claimed) return
|
||||
scope.launch {
|
||||
try {
|
||||
val params =
|
||||
buildJsonObject {
|
||||
put("id", JsonPrimitive(id))
|
||||
put(
|
||||
"answers",
|
||||
buildJsonObject {
|
||||
put(
|
||||
"answers",
|
||||
buildJsonObject {
|
||||
answers.forEach { (questionId, values) ->
|
||||
put(questionId, buildJsonObject { put("answers", JsonArray(values.map(::JsonPrimitive))) })
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
if (cancel) {
|
||||
put("cancel", JsonPrimitive(true))
|
||||
} else {
|
||||
put(
|
||||
"answers",
|
||||
buildJsonObject {
|
||||
put(
|
||||
"answers",
|
||||
buildJsonObject {
|
||||
answers.orEmpty().forEach { (questionId, values) ->
|
||||
put(questionId, buildJsonObject { put("answers", JsonArray(values.map(::JsonPrimitive))) })
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
requestGatewayBound(gatewayId, "question.resolve", params.toString())
|
||||
updateQuestions { prompts ->
|
||||
prompts.map { prompt ->
|
||||
if (prompt.record.id == id) {
|
||||
prompt.copy(
|
||||
record = prompt.record.copy(status = "answered"),
|
||||
record =
|
||||
prompt.record.copy(
|
||||
status = if (cancel) "cancelled" else "answered",
|
||||
answers =
|
||||
answers?.let { values ->
|
||||
QuestionAnswers(values.mapValues { QuestionAnswersAnswersValue(it.value) })
|
||||
},
|
||||
),
|
||||
submitting = false,
|
||||
answeredLocally = true,
|
||||
skipping = false,
|
||||
answeredLocally = !cancel,
|
||||
recoveryUnavailable = false,
|
||||
terminalObservedAtMs = prompt.terminalObservedAtMs ?: System.currentTimeMillis(),
|
||||
)
|
||||
} else {
|
||||
@@ -2144,7 +2179,11 @@ class ChatController internal constructor(
|
||||
} catch (error: Throwable) {
|
||||
updateQuestions { prompts ->
|
||||
prompts.map { prompt ->
|
||||
if (prompt.record.id == id) prompt.copy(submitting = false, errorText = error.message ?: "Question failed") else prompt
|
||||
if (prompt.record.id == id) {
|
||||
prompt.copy(submitting = false, skipping = false, errorText = error.message ?: "Question failed")
|
||||
} else {
|
||||
prompt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2153,69 +2192,163 @@ class ChatController internal constructor(
|
||||
|
||||
private fun refreshQuestions() {
|
||||
val gatewayScope = currentCacheScope()
|
||||
val (refreshGeneration, stateRevision) =
|
||||
val refreshGeneration =
|
||||
synchronized(questionStateLock) {
|
||||
questionRefreshGeneration += 1
|
||||
questionRefreshGeneration to questionStateRevision
|
||||
questionRefreshGeneration
|
||||
}
|
||||
scope.launch {
|
||||
try {
|
||||
val response = requestGatewayBound(gatewayScope?.gatewayId, "question.list", "{}")
|
||||
if (gatewayScope != currentCacheScope()) return@launch
|
||||
val records = json.decodeFromString<QuestionListResult>(response).questions
|
||||
synchronized(questionStateLock) {
|
||||
if (refreshGeneration != questionRefreshGeneration || stateRevision != questionStateRevision) {
|
||||
return@synchronized
|
||||
}
|
||||
val current = _questions.value
|
||||
val existing = current.associateBy { it.record.id }
|
||||
val nowMs = System.currentTimeMillis()
|
||||
val listedIds = records.mapTo(mutableSetOf()) { it.id }
|
||||
val retainedTerminal = current.filter { it.record.id !in listedIds && it.shouldRetainAfterList(nowMs) }
|
||||
val next =
|
||||
records.map { record ->
|
||||
existing[record.id]?.let { prompt ->
|
||||
prompt.copy(
|
||||
record = record,
|
||||
submitting = prompt.submitting && record.status == "pending",
|
||||
terminalObservedAtMs =
|
||||
if (record.status == "pending" && nowMs < record.expiresAtMs) {
|
||||
null
|
||||
} else {
|
||||
prompt.terminalObservedAtMs ?: nowMs
|
||||
},
|
||||
)
|
||||
} ?: ChatQuestionPrompt(
|
||||
record = record,
|
||||
terminalObservedAtMs = nowMs.takeIf { record.status != "pending" },
|
||||
)
|
||||
} + retainedTerminal
|
||||
if (next != current) {
|
||||
_questions.value = next
|
||||
questionStateRevision += 1
|
||||
}
|
||||
syncQuestionEvictionsLocked()
|
||||
var retryIndex = 0
|
||||
var retryStateRevision: Long? = null
|
||||
while (true) {
|
||||
val expectedStateRevision = questionRefreshCurrentRevision(refreshGeneration, gatewayScope) ?: return@launch
|
||||
if (retryStateRevision != expectedStateRevision) {
|
||||
retryStateRevision = expectedStateRevision
|
||||
retryIndex = 0
|
||||
}
|
||||
} catch (_: Throwable) {
|
||||
// Older gateways and tokens without operator.questions keep ordinary chat usable.
|
||||
val complete =
|
||||
runCatching {
|
||||
refreshQuestions(refreshGeneration, expectedStateRevision, gatewayScope)
|
||||
}.getOrDefault(false)
|
||||
if (complete) return@launch
|
||||
val currentStateRevision = questionRefreshCurrentRevision(refreshGeneration, gatewayScope) ?: return@launch
|
||||
if (currentStateRevision != expectedStateRevision) {
|
||||
// A local mutation invalidates the whole lookup snapshot, not one transport attempt.
|
||||
// Restart the bounded budget so the last attempt cannot strand another question.
|
||||
retryStateRevision = currentStateRevision
|
||||
retryIndex = 0
|
||||
}
|
||||
val retryDelayMs = QUESTION_REFRESH_RETRY_DELAYS_MS.getOrNull(retryIndex) ?: return@launch
|
||||
retryIndex += 1
|
||||
delay(retryDelayMs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun refreshQuestions(
|
||||
refreshGeneration: Long,
|
||||
stateRevision: Long,
|
||||
gatewayScope: ChatCacheScope?,
|
||||
): Boolean {
|
||||
val response = requestGatewayBound(gatewayScope?.gatewayId, "question.list", "{}")
|
||||
if (!questionRefreshIsCurrent(refreshGeneration, stateRevision, gatewayScope)) return false
|
||||
val listedRecords = json.decodeFromString<QuestionListResult>(response).questions
|
||||
val listedIds = listedRecords.mapTo(mutableSetOf()) { it.id }
|
||||
val missingPendingRecords =
|
||||
synchronized(questionStateLock) {
|
||||
if (!questionRefreshIsCurrentLocked(refreshGeneration, stateRevision)) return false
|
||||
_questions.value
|
||||
.filter { prompt ->
|
||||
prompt.record.id !in listedIds &&
|
||||
prompt.record.status == "pending" &&
|
||||
!prompt.recoveryUnavailable
|
||||
}.map { it.record }
|
||||
}
|
||||
val fallbackRecords = mutableListOf<QuestionRecord>()
|
||||
val unresolvedIds = mutableSetOf<String>()
|
||||
val unavailableIds = mutableSetOf<String>()
|
||||
for (record in missingPendingRecords) {
|
||||
val params = buildJsonObject { put("id", JsonPrimitive(record.id)) }
|
||||
try {
|
||||
val fallback = requestGatewayBound(gatewayScope?.gatewayId, "question.get", params.toString())
|
||||
fallbackRecords += json.decodeFromString<QuestionGetResult>(fallback).question
|
||||
} catch (err: CancellationException) {
|
||||
throw err
|
||||
} catch (err: GatewayRequestRejected) {
|
||||
if (err.gatewayError.details?.reason == "QUESTION_NOT_FOUND") {
|
||||
// The terminal tombstone has aged out, so the question is no longer actionable,
|
||||
// but its answered/cancelled/expired outcome cannot be reconstructed.
|
||||
unavailableIds += record.id
|
||||
} else {
|
||||
unresolvedIds += record.id
|
||||
}
|
||||
} catch (_: Throwable) {
|
||||
unresolvedIds += record.id
|
||||
}
|
||||
}
|
||||
if (!questionRefreshIsCurrent(refreshGeneration, stateRevision, gatewayScope)) return false
|
||||
val records = listedRecords + fallbackRecords.filter { it.id !in listedIds }
|
||||
return synchronized(questionStateLock) {
|
||||
if (!questionRefreshIsCurrentLocked(refreshGeneration, stateRevision)) return@synchronized false
|
||||
val current = _questions.value
|
||||
val existing = current.associateBy { it.record.id }
|
||||
val nowMs = System.currentTimeMillis()
|
||||
val refreshedIds = records.mapTo(mutableSetOf()) { it.id }
|
||||
val retainedCandidates =
|
||||
current
|
||||
.filter { prompt ->
|
||||
val status = prompt.status(nowMs)
|
||||
prompt.record.id !in refreshedIds &&
|
||||
(
|
||||
prompt.record.id in unresolvedIds ||
|
||||
prompt.record.id in unavailableIds ||
|
||||
(
|
||||
status != ChatQuestionStatus.Pending &&
|
||||
status != ChatQuestionStatus.Submitting
|
||||
)
|
||||
)
|
||||
}
|
||||
val retainedPrompts =
|
||||
retainedCandidates.map { prompt ->
|
||||
if (prompt.record.id in unavailableIds) {
|
||||
prompt.copy(
|
||||
submitting = false,
|
||||
skipping = false,
|
||||
terminalObservedAtMs = prompt.terminalObservedAtMs ?: nowMs,
|
||||
recoveryUnavailable = true,
|
||||
)
|
||||
} else {
|
||||
prompt
|
||||
}
|
||||
}
|
||||
val next =
|
||||
records.map { record ->
|
||||
existing[record.id]?.let { prompt ->
|
||||
mergeQuestionPrompt(prompt, record, nowMs)
|
||||
} ?: ChatQuestionPrompt(
|
||||
record = record,
|
||||
terminalObservedAtMs = nowMs.takeIf { record.status != "pending" || nowMs >= record.expiresAtMs },
|
||||
)
|
||||
} + retainedPrompts
|
||||
if (next != current) {
|
||||
_questions.value = next
|
||||
questionStateRevision += 1
|
||||
}
|
||||
syncQuestionEvictionsLocked()
|
||||
unresolvedIds.isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
private fun questionRefreshCurrentRevision(
|
||||
refreshGeneration: Long,
|
||||
gatewayScope: ChatCacheScope?,
|
||||
): Long? {
|
||||
if (gatewayScope != currentCacheScope()) return null
|
||||
return synchronized(questionStateLock) {
|
||||
questionStateRevision.takeIf { refreshGeneration == questionRefreshGeneration }
|
||||
}
|
||||
}
|
||||
|
||||
private fun questionRefreshIsCurrent(
|
||||
refreshGeneration: Long,
|
||||
stateRevision: Long,
|
||||
gatewayScope: ChatCacheScope?,
|
||||
): Boolean =
|
||||
gatewayScope == currentCacheScope() &&
|
||||
synchronized(questionStateLock) { questionRefreshIsCurrentLocked(refreshGeneration, stateRevision) }
|
||||
|
||||
private fun questionRefreshIsCurrentLocked(
|
||||
refreshGeneration: Long,
|
||||
stateRevision: Long,
|
||||
): Boolean = refreshGeneration == questionRefreshGeneration && stateRevision == questionStateRevision
|
||||
|
||||
private fun handleQuestionRequested(payloadJson: String) {
|
||||
val record = runCatching { json.decodeFromString<QuestionRecord>(payloadJson) }.getOrNull() ?: return
|
||||
updateQuestions { prompts ->
|
||||
if (prompts.any { it.record.id == record.id }) {
|
||||
prompts.map { prompt ->
|
||||
if (prompt.record.id == record.id) {
|
||||
prompt.copy(
|
||||
record = record,
|
||||
submitting = prompt.submitting && record.status == "pending",
|
||||
// A replayed pending event must drop stale terminal retention or
|
||||
// the zero-delay eviction loop respins without ever removing it.
|
||||
terminalObservedAtMs =
|
||||
if (record.status == "pending") null else prompt.terminalObservedAtMs,
|
||||
)
|
||||
mergeQuestionPrompt(prompt, record, System.currentTimeMillis())
|
||||
} else {
|
||||
prompt
|
||||
}
|
||||
@@ -2227,17 +2360,43 @@ class ChatController internal constructor(
|
||||
refreshQuestions()
|
||||
}
|
||||
|
||||
private fun mergeQuestionPrompt(
|
||||
prompt: ChatQuestionPrompt,
|
||||
record: QuestionRecord,
|
||||
nowMs: Long,
|
||||
): ChatQuestionPrompt {
|
||||
// Gateway terminal state is monotonic. A delayed requested/list replay must not
|
||||
// make an already resolved question actionable again.
|
||||
if ((prompt.record.status != "pending" || prompt.recoveryUnavailable) && record.status == "pending") return prompt
|
||||
return prompt.copy(
|
||||
record = record.copy(answers = record.answers ?: prompt.record.answers),
|
||||
submitting = prompt.submitting && record.status == "pending",
|
||||
skipping = prompt.skipping && record.status == "pending",
|
||||
answeredLocally = prompt.answeredLocally && record.status == "answered",
|
||||
recoveryUnavailable = false,
|
||||
terminalObservedAtMs =
|
||||
if (record.status == "pending" && nowMs < record.expiresAtMs) {
|
||||
null
|
||||
} else {
|
||||
prompt.terminalObservedAtMs ?: nowMs
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleQuestionResolved(payloadJson: String) {
|
||||
val payload = runCatching { json.parseToJsonElement(payloadJson).jsonObject }.getOrNull() ?: return
|
||||
val id = payload["id"].asStringOrNull() ?: return
|
||||
val status = payload["status"].asStringOrNull() ?: return
|
||||
val answers = payload["answers"]?.let { runCatching { json.decodeFromJsonElement<QuestionAnswers>(it) }.getOrNull() }
|
||||
val nowMs = System.currentTimeMillis()
|
||||
updateQuestions { prompts ->
|
||||
prompts.map { prompt ->
|
||||
if (prompt.record.id == id) {
|
||||
prompt.copy(
|
||||
record = prompt.record.copy(status = status),
|
||||
record = prompt.record.copy(status = status, answers = answers ?: prompt.record.answers),
|
||||
submitting = false,
|
||||
skipping = false,
|
||||
recoveryUnavailable = false,
|
||||
terminalObservedAtMs = prompt.terminalObservedAtMs ?: nowMs,
|
||||
)
|
||||
} else {
|
||||
@@ -2274,49 +2433,33 @@ class ChatController internal constructor(
|
||||
if (questionEvictionJobs.containsKey(prompt.record.id)) continue
|
||||
val id = prompt.record.id
|
||||
val observedAt = prompt.terminalObservedAtMs
|
||||
val remainingMs =
|
||||
if (observedAt == null && prompt.record.status == "pending" && prompt.record.expiresAtMs != Long.MAX_VALUE) {
|
||||
(prompt.record.expiresAtMs - nowMs).coerceAtLeast(0)
|
||||
} else if (observedAt != null) {
|
||||
(QUESTION_TERMINAL_RETENTION_MS - (nowMs - observedAt)).coerceAtLeast(0)
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
if (observedAt != null || prompt.record.status != "pending" || prompt.record.expiresAtMs == Long.MAX_VALUE) continue
|
||||
val remainingMs = (prompt.record.expiresAtMs - nowMs).coerceAtLeast(0)
|
||||
val job =
|
||||
scope.launch(start = CoroutineStart.LAZY) {
|
||||
delay(remainingMs)
|
||||
var shouldRefresh = false
|
||||
synchronized(questionStateLock) {
|
||||
questionEvictionJobs.remove(id)
|
||||
val current = _questions.value
|
||||
if (observedAt == null) {
|
||||
val next =
|
||||
current.map {
|
||||
if (it.record.id == id && it.record.status == "pending" && it.terminalObservedAtMs == null) {
|
||||
it.copy(terminalObservedAtMs = System.currentTimeMillis())
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
if (next != current) {
|
||||
_questions.value = next
|
||||
questionStateRevision += 1
|
||||
}
|
||||
syncQuestionEvictionsLocked()
|
||||
return@synchronized
|
||||
}
|
||||
val next =
|
||||
current.filterNot {
|
||||
it.record.id == id &&
|
||||
it.terminalObservedAtMs == observedAt &&
|
||||
(it.status() == ChatQuestionStatus.Expired || it.record.status != "pending")
|
||||
current.map {
|
||||
if (it.record.id == id && it.record.status == "pending" && it.terminalObservedAtMs == null) {
|
||||
it.copy(terminalObservedAtMs = System.currentTimeMillis())
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
if (next != current) {
|
||||
_questions.value = next
|
||||
questionStateRevision += 1
|
||||
} else {
|
||||
syncQuestionEvictionsLocked()
|
||||
shouldRefresh = true
|
||||
}
|
||||
syncQuestionEvictionsLocked()
|
||||
}
|
||||
// The local deadline is only a presentation fallback. Reconcile outside
|
||||
// the state lock in case another surface supplied the terminal outcome.
|
||||
if (shouldRefresh) refreshQuestions()
|
||||
}
|
||||
questionEvictionJobs[id] = QuestionEvictionJob(job, observedAt)
|
||||
job.start()
|
||||
|
||||
@@ -3,8 +3,6 @@ package ai.openclaw.app.chat
|
||||
import ai.openclaw.app.gateway.Question
|
||||
import ai.openclaw.app.gateway.QuestionRecord
|
||||
|
||||
internal const val QUESTION_TERMINAL_RETENTION_MS = 15_000L
|
||||
|
||||
enum class ChatQuestionStatus {
|
||||
Pending,
|
||||
Submitting,
|
||||
@@ -12,29 +10,34 @@ enum class ChatQuestionStatus {
|
||||
AnsweredElsewhere,
|
||||
Expired,
|
||||
Cancelled,
|
||||
Unavailable,
|
||||
}
|
||||
|
||||
data class ChatQuestionPrompt(
|
||||
val record: QuestionRecord,
|
||||
val submitting: Boolean = false,
|
||||
val skipping: Boolean = false,
|
||||
val answeredLocally: Boolean = false,
|
||||
val errorText: String? = null,
|
||||
val terminalObservedAtMs: Long? = null,
|
||||
val recoveryUnavailable: Boolean = false,
|
||||
) {
|
||||
fun status(nowMs: Long = System.currentTimeMillis()): ChatQuestionStatus =
|
||||
when (record.status) {
|
||||
"answered" -> if (answeredLocally) ChatQuestionStatus.Answered else ChatQuestionStatus.AnsweredElsewhere
|
||||
"cancelled" -> ChatQuestionStatus.Cancelled
|
||||
"expired" -> ChatQuestionStatus.Expired
|
||||
else ->
|
||||
when {
|
||||
nowMs >= record.expiresAtMs -> ChatQuestionStatus.Expired
|
||||
submitting -> ChatQuestionStatus.Submitting
|
||||
else -> ChatQuestionStatus.Pending
|
||||
}
|
||||
if (recoveryUnavailable) {
|
||||
ChatQuestionStatus.Unavailable
|
||||
} else {
|
||||
when (record.status) {
|
||||
"answered" -> if (answeredLocally) ChatQuestionStatus.Answered else ChatQuestionStatus.AnsweredElsewhere
|
||||
"cancelled" -> ChatQuestionStatus.Cancelled
|
||||
"expired" -> ChatQuestionStatus.Expired
|
||||
else ->
|
||||
when {
|
||||
nowMs >= record.expiresAtMs -> ChatQuestionStatus.Expired
|
||||
submitting -> ChatQuestionStatus.Submitting
|
||||
else -> ChatQuestionStatus.Pending
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun shouldRetainAfterList(nowMs: Long): Boolean = terminalObservedAtMs?.let { nowMs - it < QUESTION_TERMINAL_RETENTION_MS } == true
|
||||
}
|
||||
|
||||
data class ChatQuestionDraft(
|
||||
|
||||
@@ -108,6 +108,11 @@ data class QuestionRecord(
|
||||
val resolvedBy: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class QuestionGetResult(
|
||||
val question: QuestionRecord,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class QuestionListResult(
|
||||
val questions: List<QuestionRecord>,
|
||||
|
||||
@@ -231,7 +231,6 @@ internal val nativeStringResourceIds: Map<String, Int> =
|
||||
"Camera enabled" to R.string.native_29d5c526bab8bf50,
|
||||
"Cancel" to R.string.cancel,
|
||||
"Cancel voice note" to R.string.native_fae8fa3bc050ab7e,
|
||||
"Cancelled" to R.string.native_d353a99eb4556847,
|
||||
"Cannot reach gateway" to R.string.native_d8f247890b029a85,
|
||||
"Canvas" to R.string.native_3824a9f4dafe92c6,
|
||||
"Canvas Status" to R.string.native_584162ac73e99ee6,
|
||||
@@ -485,8 +484,6 @@ internal val nativeStringResourceIds: Map<String, Int> =
|
||||
"Expand plan checklist" to R.string.native_d30f33731adb6b40,
|
||||
"Expired" to R.string.native_424a2551d356754c,
|
||||
"Expires \${duration}" to R.string.native_48377001408f13be,
|
||||
"Expires in \${seconds / 60}m \${seconds % 60}s" to R.string.native_9dca3c2b01b40c0b,
|
||||
"Expires in \${seconds}s" to R.string.native_e9e3a4b23345cb57,
|
||||
"Expression" to R.string.native_c67415bcff328a59,
|
||||
"Extra" to R.string.native_2e4aaf05df1cbfb3,
|
||||
"Failed" to R.string.native_031a8f0f659df890,
|
||||
@@ -1140,6 +1137,7 @@ internal val nativeStringResourceIds: Map<String, Int> =
|
||||
"Skill detail is not available in the current skills status." to R.string.native_90bf81305006e6ed,
|
||||
"Skills" to R.string.native_66d0f523a379b2de,
|
||||
"Skills installed on the gateway will appear here." to R.string.native_4d5e590b23745b4d,
|
||||
"Skip" to R.string.native_28d03596d24eeb4e,
|
||||
"Skipped" to R.string.native_12698ce1ea5cd4ab,
|
||||
"Smart delivery" to R.string.native_ab3c45669917ec6f,
|
||||
"Some channel status checks did not complete." to R.string.native_64f24041b18e3654,
|
||||
|
||||
@@ -21,6 +21,7 @@ import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -38,12 +39,17 @@ import kotlinx.coroutines.delay
|
||||
internal fun ChatQuestionCard(
|
||||
prompt: ChatQuestionPrompt,
|
||||
onSubmit: (String, Map<String, List<String>>) -> Unit,
|
||||
onSkip: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var draft by remember(prompt.record.id) { mutableStateOf(ChatQuestionDraft()) }
|
||||
var nowMs by remember(prompt.record.id) { mutableLongStateOf(System.currentTimeMillis()) }
|
||||
val status = prompt.status(nowMs)
|
||||
val pending = status == ChatQuestionStatus.Pending
|
||||
if (!pending && status != ChatQuestionStatus.Submitting) {
|
||||
ChatQuestionSummary(prompt = prompt, status = status, modifier = modifier)
|
||||
return
|
||||
}
|
||||
LaunchedEffect(prompt.record.id, prompt.record.expiresAtMs, status) {
|
||||
while (status == ChatQuestionStatus.Pending || status == ChatQuestionStatus.Submitting) {
|
||||
delay(1000)
|
||||
@@ -75,11 +81,47 @@ internal fun ChatQuestionCard(
|
||||
status = status,
|
||||
nowMs = nowMs,
|
||||
onSubmit = onSubmit,
|
||||
onSkip = onSkip,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChatQuestionSummary(
|
||||
prompt: ChatQuestionPrompt,
|
||||
status: ChatQuestionStatus,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Surface(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(ClawTheme.radii.row),
|
||||
color = ClawTheme.colors.surfaceRaised,
|
||||
border = BorderStroke(1.dp, ClawTheme.colors.border),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
prompt.record.questions.forEach { question ->
|
||||
Column {
|
||||
Text(
|
||||
text = question.header + ':',
|
||||
style = ClawTheme.type.caption,
|
||||
color = ClawTheme.colors.text,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
Text(
|
||||
text = terminalQuestionAnswer(prompt, question, status),
|
||||
style = ClawTheme.type.caption,
|
||||
color = ClawTheme.colors.textMuted,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun QuestionSection(
|
||||
question: Question,
|
||||
@@ -143,6 +185,7 @@ private fun QuestionFooter(
|
||||
status: ChatQuestionStatus,
|
||||
nowMs: Long,
|
||||
onSubmit: (String, Map<String, List<String>>) -> Unit,
|
||||
onSkip: (String) -> Unit,
|
||||
) {
|
||||
val answers = draft.answers(prompt.record.questions)
|
||||
if (status == ChatQuestionStatus.Pending || status == ChatQuestionStatus.Submitting) {
|
||||
@@ -153,44 +196,51 @@ private fun QuestionFooter(
|
||||
color = ClawTheme.colors.textMuted,
|
||||
)
|
||||
Spacer(Modifier.weight(1f))
|
||||
TextButton(
|
||||
onClick = { onSkip(prompt.record.id) },
|
||||
enabled = status == ChatQuestionStatus.Pending,
|
||||
) {
|
||||
Text(nativeString("Skip"))
|
||||
}
|
||||
Button(
|
||||
onClick = { answers?.let { onSubmit(prompt.record.id, it) } },
|
||||
enabled = answers != null && status == ChatQuestionStatus.Pending,
|
||||
) {
|
||||
Text(if (status == ChatQuestionStatus.Submitting) nativeString("Submitting…") else nativeString("Submit"))
|
||||
Text(
|
||||
if (status == ChatQuestionStatus.Submitting && !prompt.skipping) {
|
||||
nativeString("Submitting…")
|
||||
} else {
|
||||
nativeString("Submit")
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
prompt.errorText?.let { error ->
|
||||
Text(text = error, style = ClawTheme.type.caption, color = ClawTheme.colors.danger)
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
text =
|
||||
when (status) {
|
||||
ChatQuestionStatus.Answered -> nativeString("Answered")
|
||||
ChatQuestionStatus.AnsweredElsewhere -> nativeString("Answered elsewhere")
|
||||
ChatQuestionStatus.Expired -> nativeString("Expired")
|
||||
ChatQuestionStatus.Cancelled -> nativeString("Cancelled")
|
||||
ChatQuestionStatus.Pending -> nativeString("Pending")
|
||||
ChatQuestionStatus.Submitting -> nativeString("Submitting…")
|
||||
},
|
||||
style = ClawTheme.type.caption,
|
||||
color = ClawTheme.colors.textMuted,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun terminalQuestionAnswer(
|
||||
prompt: ChatQuestionPrompt,
|
||||
question: Question,
|
||||
status: ChatQuestionStatus,
|
||||
): String {
|
||||
if (status == ChatQuestionStatus.Cancelled) return nativeString("Skipped")
|
||||
if (status == ChatQuestionStatus.Expired) return nativeString("Expired")
|
||||
if (status == ChatQuestionStatus.Unavailable) return nativeString("Unavailable")
|
||||
prompt.record.answers?.answers?.get(question.id)?.answers?.takeIf { it.isNotEmpty() }?.let {
|
||||
return it.joinToString(", ")
|
||||
}
|
||||
return if (status == ChatQuestionStatus.AnsweredElsewhere) nativeString("Answered elsewhere") else nativeString("Answered")
|
||||
}
|
||||
|
||||
// nativeString is the non-composable resource accessor (nativeStringResource
|
||||
// is the @Composable variant), so this helper is safe outside composition.
|
||||
private fun questionCountdown(
|
||||
internal fun questionCountdown(
|
||||
expiresAtMs: Long,
|
||||
nowMs: Long,
|
||||
): String {
|
||||
val seconds = ((expiresAtMs - nowMs).coerceAtLeast(0) + 999) / 1000
|
||||
return if (seconds >= 60) {
|
||||
nativeString("Expires in \${seconds / 60}m \${seconds % 60}s", seconds / 60, seconds % 60)
|
||||
} else {
|
||||
nativeString("Expires in \${seconds}s", seconds)
|
||||
}
|
||||
return (seconds / 60).toString() + ':' + (seconds % 60).toString().padStart(2, '0')
|
||||
}
|
||||
|
||||
@@ -583,6 +583,7 @@ fun ChatScreen(
|
||||
onRetryOutbox = viewModel::retryChatOutboxCommand,
|
||||
onDeleteOutbox = viewModel::deleteChatOutboxCommand,
|
||||
onResolveQuestion = viewModel::resolveChatQuestion,
|
||||
onSkipQuestion = viewModel::skipChatQuestion,
|
||||
onStarterPrompt = { prompt -> inputDrafts[composerOwner] = prompt },
|
||||
onReplyMessage = { value -> viewModel.setChatReplyDraft(value, composerOwner) },
|
||||
speechState = messageSpeechState,
|
||||
@@ -1044,6 +1045,7 @@ private fun ChatMessageList(
|
||||
onRetryOutbox: (String) -> Unit,
|
||||
onDeleteOutbox: (String) -> Unit,
|
||||
onResolveQuestion: (String, Map<String, List<String>>) -> Unit,
|
||||
onSkipQuestion: (String) -> Unit,
|
||||
onStarterPrompt: (String) -> Unit,
|
||||
onReplyMessage: (String) -> Unit,
|
||||
speechState: MessageSpeechState?,
|
||||
@@ -1117,7 +1119,7 @@ private fun ChatMessageList(
|
||||
)
|
||||
is ChatTimelineItem.PendingTools -> ToolBubble(toolCalls = item.toolCalls)
|
||||
is ChatTimelineItem.QuestionPrompt ->
|
||||
ChatQuestionCard(prompt = item.prompt, onSubmit = onResolveQuestion)
|
||||
ChatQuestionCard(prompt = item.prompt, onSubmit = onResolveQuestion, onSkip = onSkipQuestion)
|
||||
is ChatTimelineItem.StreamingAssistant ->
|
||||
ChatBubble(
|
||||
messageId = null,
|
||||
|
||||
@@ -251,9 +251,15 @@ private fun latestContentVersion(
|
||||
questions.forEach { prompt ->
|
||||
append(prompt.record.id)
|
||||
append(',')
|
||||
append(prompt.record.status)
|
||||
append(prompt.status())
|
||||
append(',')
|
||||
append(prompt.submitting)
|
||||
append(',')
|
||||
append(prompt.skipping)
|
||||
append(',')
|
||||
append(prompt.errorText?.hashCode() ?: 0)
|
||||
append(',')
|
||||
append(prompt.record.answers.hashCode())
|
||||
append(';')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,6 +247,7 @@
|
||||
<string name="native_2821f0168d6dd9df" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"حوّل هذا الجهاز إلى عقدة OpenClaw آمنة للدردشة والصوت والكاميرا وأدوات الجهاز."</string>
|
||||
<string name="native_2831546121359362" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"إعداد يدوي"</string>
|
||||
<string name="native_28bb849671cab958" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"تلميح: أوقف الاستماع لإرسال الجزء الملتقط."</string>
|
||||
<string name="native_28d03596d24eeb4e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"تخطي"</string>
|
||||
<string name="native_28d55ec6974d84e8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"فشل الطلب الصوتي"</string>
|
||||
<string name="native_29129d736e62995a" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"سيؤدي هذا إلى رفض \"%1$s\" وتحديث حالة ورشة Skills من Gateway."</string>
|
||||
<string name="native_2937013f21818106" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"update"</string>
|
||||
@@ -924,7 +925,6 @@
|
||||
<string name="native_9d60841e0a7891df" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"تحقق"</string>
|
||||
<string name="native_9d988c13c67e88e6" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"قراءة موقع هذا الهاتف"</string>
|
||||
<string name="native_9db0df0afb47b562" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"مفتاح المهارة"</string>
|
||||
<string name="native_9dca3c2b01b40c0b" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"تنتهي الصلاحية خلال %1$sد %2$sث"</string>
|
||||
<string name="native_9e37aa0704a3cf44" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"تم تثبيت %1$s."</string>
|
||||
<string name="native_9e4635769f0aaf0d" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"بوابات Gateway"</string>
|
||||
<string name="native_9e9bef85bb320b81" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"وضع إعادة التوجيه"</string>
|
||||
@@ -1221,7 +1221,6 @@
|
||||
<string name="native_d2ee46edf00c8eb6" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"... +%1$s أخرى"</string>
|
||||
<string name="native_d30f33731adb6b40" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"توسيع قائمة التحقق الخاصة بالخطة"</string>
|
||||
<string name="native_d31de1a5c5c8ba2a" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"المتصفح"</string>
|
||||
<string name="native_d353a99eb4556847" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"تم الإلغاء"</string>
|
||||
<string name="native_d354ee936f59115e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"screen record"</string>
|
||||
<string name="native_d3974fd6d3b6c81f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"التشغيل معلّق"</string>
|
||||
<string name="native_d3f75e24411053c4" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"ذكاء اصطناعي شخصي على أجهزتك"</string>
|
||||
@@ -1344,7 +1343,6 @@
|
||||
<string name="native_e89bdf093e8a6756" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"المزوّدون والنماذج"</string>
|
||||
<string name="native_e93372533f323b2f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"العقدة"</string>
|
||||
<string name="native_e98feb46bd5d7792" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"%1$s "</string>
|
||||
<string name="native_e9e3a4b23345cb57" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"تنتهي الصلاحية خلال %1$sث"</string>
|
||||
<string name="native_ea14eb224e89a810" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"الموجّه غير متاح"</string>
|
||||
<string name="native_ea2100dc89ae9fe2" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"السجلات"</string>
|
||||
<string name="native_ea5cddf7a04d7b94" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"اتصل بـ Gateway لفحص مقترحات ورشة عمل Skills."</string>
|
||||
|
||||
@@ -247,6 +247,7 @@
|
||||
<string name="native_2821f0168d6dd9df" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Verwandeln Sie dieses Gerät in einen sicheren OpenClaw-Knoten für Chat, Sprache, Kamera und Gerätetools."</string>
|
||||
<string name="native_2831546121359362" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Manuelle Einrichtung"</string>
|
||||
<string name="native_28bb849671cab958" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Tipp: Beenden Sie das Zuhören, um den erfassten Beitrag zu senden."</string>
|
||||
<string name="native_28d03596d24eeb4e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Überspringen"</string>
|
||||
<string name="native_28d55ec6974d84e8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Sprachanfrage fehlgeschlagen"</string>
|
||||
<string name="native_29129d736e62995a" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Dadurch wird \"%1$s\" abgelehnt und der Status von Skill Workshop über das Gateway aktualisiert."</string>
|
||||
<string name="native_2937013f21818106" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"update"</string>
|
||||
@@ -924,7 +925,6 @@
|
||||
<string name="native_9d60841e0a7891df" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Prüfen"</string>
|
||||
<string name="native_9d988c13c67e88e6" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Standort dieses Telefons abrufen"</string>
|
||||
<string name="native_9db0df0afb47b562" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Skill-Schlüssel"</string>
|
||||
<string name="native_9dca3c2b01b40c0b" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Läuft in %1$sm %2$ss ab"</string>
|
||||
<string name="native_9e37aa0704a3cf44" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"%1$s wurde installiert."</string>
|
||||
<string name="native_9e4635769f0aaf0d" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateways"</string>
|
||||
<string name="native_9e9bef85bb320b81" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Weiterleitungsmodus"</string>
|
||||
@@ -1221,7 +1221,6 @@
|
||||
<string name="native_d2ee46edf00c8eb6" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"... +%1$s weitere"</string>
|
||||
<string name="native_d30f33731adb6b40" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Plan-Checkliste ausklappen"</string>
|
||||
<string name="native_d31de1a5c5c8ba2a" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Browser"</string>
|
||||
<string name="native_d353a99eb4556847" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Abgebrochen"</string>
|
||||
<string name="native_d354ee936f59115e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"screen record"</string>
|
||||
<string name="native_d3974fd6d3b6c81f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Ausführung ausstehend"</string>
|
||||
<string name="native_d3f75e24411053c4" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Persönliche KI auf deinen Geräten"</string>
|
||||
@@ -1344,7 +1343,6 @@
|
||||
<string name="native_e89bdf093e8a6756" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Anbieter & Modelle"</string>
|
||||
<string name="native_e93372533f323b2f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Knoten"</string>
|
||||
<string name="native_e98feb46bd5d7792" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"%1$s "</string>
|
||||
<string name="native_e9e3a4b23345cb57" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Läuft in %1$ss ab"</string>
|
||||
<string name="native_ea14eb224e89a810" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Prompt nicht verfügbar"</string>
|
||||
<string name="native_ea2100dc89ae9fe2" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Protokolle"</string>
|
||||
<string name="native_ea5cddf7a04d7b94" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Verbinden Sie das Gateway, um Vorschläge aus Skill Workshop zu prüfen."</string>
|
||||
|
||||
@@ -247,6 +247,7 @@
|
||||
<string name="native_2821f0168d6dd9df" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Convierte este dispositivo en un nodo seguro de OpenClaw para chat, voz, cámara y herramientas del dispositivo."</string>
|
||||
<string name="native_2831546121359362" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Configuración manual"</string>
|
||||
<string name="native_28bb849671cab958" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Consejo: deja de escuchar para enviar el turno capturado."</string>
|
||||
<string name="native_28d03596d24eeb4e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Omitir"</string>
|
||||
<string name="native_28d55ec6974d84e8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Error en la solicitud de voz"</string>
|
||||
<string name="native_29129d736e62995a" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Esto rechazará \"%1$s\" y actualizará el estado de Skill Workshop desde el Gateway."</string>
|
||||
<string name="native_2937013f21818106" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"update"</string>
|
||||
@@ -924,7 +925,6 @@
|
||||
<string name="native_9d60841e0a7891df" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Comprobar"</string>
|
||||
<string name="native_9d988c13c67e88e6" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Leer la ubicación de este teléfono"</string>
|
||||
<string name="native_9db0df0afb47b562" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Clave de Skill"</string>
|
||||
<string name="native_9dca3c2b01b40c0b" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Caduca en %1$sm %2$ss"</string>
|
||||
<string name="native_9e37aa0704a3cf44" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Se instaló %1$s."</string>
|
||||
<string name="native_9e4635769f0aaf0d" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateways"</string>
|
||||
<string name="native_9e9bef85bb320b81" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Modo de reenvío"</string>
|
||||
@@ -1221,7 +1221,6 @@
|
||||
<string name="native_d2ee46edf00c8eb6" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"... +%1$s más"</string>
|
||||
<string name="native_d30f33731adb6b40" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Expandir la lista de verificación del plan"</string>
|
||||
<string name="native_d31de1a5c5c8ba2a" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Navegador"</string>
|
||||
<string name="native_d353a99eb4556847" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Cancelada"</string>
|
||||
<string name="native_d354ee936f59115e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"screen record"</string>
|
||||
<string name="native_d3974fd6d3b6c81f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Ejecución pendiente"</string>
|
||||
<string name="native_d3f75e24411053c4" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"IA personal en tus dispositivos"</string>
|
||||
@@ -1344,7 +1343,6 @@
|
||||
<string name="native_e89bdf093e8a6756" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Proveedores y modelos"</string>
|
||||
<string name="native_e93372533f323b2f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Nodo"</string>
|
||||
<string name="native_e98feb46bd5d7792" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"%1$s "</string>
|
||||
<string name="native_e9e3a4b23345cb57" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Caduca en %1$ss"</string>
|
||||
<string name="native_ea14eb224e89a810" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Indicación no disponible"</string>
|
||||
<string name="native_ea2100dc89ae9fe2" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Registros"</string>
|
||||
<string name="native_ea5cddf7a04d7b94" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Conecta el Gateway para consultar las propuestas de Skill Workshop."</string>
|
||||
|
||||
@@ -247,6 +247,7 @@
|
||||
<string name="native_2821f0168d6dd9df" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"این دستگاه را به یک گره امن OpenClaw برای چت، صدا، دوربین و ابزارهای دستگاه تبدیل کنید."</string>
|
||||
<string name="native_2831546121359362" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"راهاندازی دستی"</string>
|
||||
<string name="native_28bb849671cab958" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"نکته: برای ارسال نوبت ضبطشده، گوش دادن را متوقف کنید."</string>
|
||||
<string name="native_28d03596d24eeb4e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"رد کردن"</string>
|
||||
<string name="native_28d55ec6974d84e8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"درخواست صوتی ناموفق بود"</string>
|
||||
<string name="native_29129d736e62995a" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"این کار \"%1$s\" را رد میکند و وضعیت Skill Workshop را از Gateway تازهسازی میکند."</string>
|
||||
<string name="native_2937013f21818106" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"update"</string>
|
||||
@@ -924,7 +925,6 @@
|
||||
<string name="native_9d60841e0a7891df" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"بررسی"</string>
|
||||
<string name="native_9d988c13c67e88e6" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"خواندن موقعیت مکانی این تلفن"</string>
|
||||
<string name="native_9db0df0afb47b562" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"کلید مهارت"</string>
|
||||
<string name="native_9dca3c2b01b40c0b" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"انقضا تا %1$sm %2$ss دیگر"</string>
|
||||
<string name="native_9e37aa0704a3cf44" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"%1$s نصب شد."</string>
|
||||
<string name="native_9e4635769f0aaf0d" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gatewayها"</string>
|
||||
<string name="native_9e9bef85bb320b81" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"حالت بازارسال"</string>
|
||||
@@ -1221,7 +1221,6 @@
|
||||
<string name="native_d2ee46edf00c8eb6" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"... +%1$s مورد دیگر"</string>
|
||||
<string name="native_d30f33731adb6b40" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"بازکردن فهرست بررسی برنامه"</string>
|
||||
<string name="native_d31de1a5c5c8ba2a" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"مرورگر"</string>
|
||||
<string name="native_d353a99eb4556847" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"لغو شده"</string>
|
||||
<string name="native_d354ee936f59115e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"screen record"</string>
|
||||
<string name="native_d3974fd6d3b6c81f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"اجرای در انتظار"</string>
|
||||
<string name="native_d3f75e24411053c4" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"هوش مصنوعی شخصی روی دستگاههای شما"</string>
|
||||
@@ -1344,7 +1343,6 @@
|
||||
<string name="native_e89bdf093e8a6756" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"ارائهدهندگان و مدلها"</string>
|
||||
<string name="native_e93372533f323b2f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"گره"</string>
|
||||
<string name="native_e98feb46bd5d7792" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"%1$s "</string>
|
||||
<string name="native_e9e3a4b23345cb57" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"انقضا تا %1$ss دیگر"</string>
|
||||
<string name="native_ea14eb224e89a810" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"درخواست در دسترس نیست"</string>
|
||||
<string name="native_ea2100dc89ae9fe2" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"گزارشها"</string>
|
||||
<string name="native_ea5cddf7a04d7b94" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"برای بررسی پیشنهادهای Skill Workshop، به Gateway متصل شوید."</string>
|
||||
|
||||
@@ -247,6 +247,7 @@
|
||||
<string name="native_2821f0168d6dd9df" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Transformez cet appareil en nœud OpenClaw sécurisé pour le chat, la voix, la caméra et les outils de l’appareil."</string>
|
||||
<string name="native_2831546121359362" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Configuration manuelle"</string>
|
||||
<string name="native_28bb849671cab958" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Astuce : arrêtez l’écoute pour envoyer l’intervention capturée."</string>
|
||||
<string name="native_28d03596d24eeb4e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Ignorer"</string>
|
||||
<string name="native_28d55ec6974d84e8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Échec de la requête vocale"</string>
|
||||
<string name="native_29129d736e62995a" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Cela rejettera \"%1$s\" et actualisera l’état de Skill Workshop depuis le Gateway."</string>
|
||||
<string name="native_2937013f21818106" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"update"</string>
|
||||
@@ -924,7 +925,6 @@
|
||||
<string name="native_9d60841e0a7891df" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Vérifier"</string>
|
||||
<string name="native_9d988c13c67e88e6" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Lire la position de ce téléphone"</string>
|
||||
<string name="native_9db0df0afb47b562" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Clé du Skill"</string>
|
||||
<string name="native_9dca3c2b01b40c0b" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Expire dans %1$s min %2$s s"</string>
|
||||
<string name="native_9e37aa0704a3cf44" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"%1$s a été installé."</string>
|
||||
<string name="native_9e4635769f0aaf0d" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateways"</string>
|
||||
<string name="native_9e9bef85bb320b81" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Mode de transfert"</string>
|
||||
@@ -1221,7 +1221,6 @@
|
||||
<string name="native_d2ee46edf00c8eb6" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"... +%1$s de plus"</string>
|
||||
<string name="native_d30f33731adb6b40" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Développer la liste de contrôle du plan"</string>
|
||||
<string name="native_d31de1a5c5c8ba2a" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Navigateur"</string>
|
||||
<string name="native_d353a99eb4556847" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Annulé"</string>
|
||||
<string name="native_d354ee936f59115e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"screen record"</string>
|
||||
<string name="native_d3974fd6d3b6c81f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Exécution en attente"</string>
|
||||
<string name="native_d3f75e24411053c4" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"IA personnelle sur vos appareils"</string>
|
||||
@@ -1344,7 +1343,6 @@
|
||||
<string name="native_e89bdf093e8a6756" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Fournisseurs et modèles"</string>
|
||||
<string name="native_e93372533f323b2f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Nœud"</string>
|
||||
<string name="native_e98feb46bd5d7792" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"%1$s "</string>
|
||||
<string name="native_e9e3a4b23345cb57" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Expire dans %1$s s"</string>
|
||||
<string name="native_ea14eb224e89a810" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Invite indisponible"</string>
|
||||
<string name="native_ea2100dc89ae9fe2" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Journaux"</string>
|
||||
<string name="native_ea5cddf7a04d7b94" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Connectez le Gateway pour examiner les propositions de Skill Workshop."</string>
|
||||
|
||||
@@ -247,6 +247,7 @@
|
||||
<string name="native_2821f0168d6dd9df" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"इस डिवाइस को chat, voice, camera, और device tools के लिए सुरक्षित OpenClaw नोड में बदलें।"</string>
|
||||
<string name="native_2831546121359362" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"मैनुअल सेटअप"</string>
|
||||
<string name="native_28bb849671cab958" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"सुझाव: कैप्चर किया गया टर्न भेजने के लिए सुनना बंद करें।"</string>
|
||||
<string name="native_28d03596d24eeb4e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"छोड़ें"</string>
|
||||
<string name="native_28d55ec6974d84e8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"वॉइस अनुरोध विफल रहा"</string>
|
||||
<string name="native_29129d736e62995a" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"यह \"%1$s\" अस्वीकार करेगा और Gateway से Skill Workshop की स्थिति रीफ़्रेश करेगा।"</string>
|
||||
<string name="native_2937013f21818106" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"update"</string>
|
||||
@@ -924,7 +925,6 @@
|
||||
<string name="native_9d60841e0a7891df" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"जाँचें"</string>
|
||||
<string name="native_9d988c13c67e88e6" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"इस फ़ोन का स्थान पढ़ें"</string>
|
||||
<string name="native_9db0df0afb47b562" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Skill कुंजी"</string>
|
||||
<string name="native_9dca3c2b01b40c0b" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"%1$sm %2$ss में समाप्त होगा"</string>
|
||||
<string name="native_9e37aa0704a3cf44" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"%1$s इंस्टॉल किया गया।"</string>
|
||||
<string name="native_9e4635769f0aaf0d" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateways"</string>
|
||||
<string name="native_9e9bef85bb320b81" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"फ़ॉरवर्डिंग मोड"</string>
|
||||
@@ -1221,7 +1221,6 @@
|
||||
<string name="native_d2ee46edf00c8eb6" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"... +%1$s और"</string>
|
||||
<string name="native_d30f33731adb6b40" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"योजना चेकलिस्ट विस्तृत करें"</string>
|
||||
<string name="native_d31de1a5c5c8ba2a" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Browser"</string>
|
||||
<string name="native_d353a99eb4556847" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"रद्द किया गया"</string>
|
||||
<string name="native_d354ee936f59115e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"screen record"</string>
|
||||
<string name="native_d3974fd6d3b6c81f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"रन लंबित है"</string>
|
||||
<string name="native_d3f75e24411053c4" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"आपके डिवाइसों पर निजी AI"</string>
|
||||
@@ -1344,7 +1343,6 @@
|
||||
<string name="native_e89bdf093e8a6756" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"प्रदाता और मॉडल"</string>
|
||||
<string name="native_e93372533f323b2f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"नोड"</string>
|
||||
<string name="native_e98feb46bd5d7792" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"%1$s "</string>
|
||||
<string name="native_e9e3a4b23345cb57" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"%1$ss में समाप्त होगा"</string>
|
||||
<string name="native_ea14eb224e89a810" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"प्रॉम्प्ट उपलब्ध नहीं है"</string>
|
||||
<string name="native_ea2100dc89ae9fe2" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"लॉग"</string>
|
||||
<string name="native_ea5cddf7a04d7b94" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Skill Workshop प्रस्तावों की जाँच करने के लिए Gateway कनेक्ट करें।"</string>
|
||||
|
||||
@@ -247,6 +247,7 @@
|
||||
<string name="native_2821f0168d6dd9df" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Ubah perangkat ini menjadi node OpenClaw yang aman untuk chat, suara, kamera, dan alat perangkat."</string>
|
||||
<string name="native_2831546121359362" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Penyiapan manual"</string>
|
||||
<string name="native_28bb849671cab958" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Tips: hentikan mendengarkan untuk mengirim giliran yang direkam."</string>
|
||||
<string name="native_28d03596d24eeb4e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Lewati"</string>
|
||||
<string name="native_28d55ec6974d84e8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Permintaan suara gagal"</string>
|
||||
<string name="native_29129d736e62995a" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Tindakan ini akan menolak \"%1$s\" dan memuat ulang status Skill Workshop dari Gateway."</string>
|
||||
<string name="native_2937013f21818106" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"update"</string>
|
||||
@@ -924,7 +925,6 @@
|
||||
<string name="native_9d60841e0a7891df" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Periksa"</string>
|
||||
<string name="native_9d988c13c67e88e6" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Baca lokasi ponsel ini"</string>
|
||||
<string name="native_9db0df0afb47b562" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Kunci Skill"</string>
|
||||
<string name="native_9dca3c2b01b40c0b" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Kedaluwarsa dalam %1$sm %2$ss"</string>
|
||||
<string name="native_9e37aa0704a3cf44" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"%1$s telah diinstal."</string>
|
||||
<string name="native_9e4635769f0aaf0d" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway"</string>
|
||||
<string name="native_9e9bef85bb320b81" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Mode Penerusan"</string>
|
||||
@@ -1221,7 +1221,6 @@
|
||||
<string name="native_d2ee46edf00c8eb6" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"... +%1$s lagi"</string>
|
||||
<string name="native_d30f33731adb6b40" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Luaskan daftar periksa rencana"</string>
|
||||
<string name="native_d31de1a5c5c8ba2a" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Browser"</string>
|
||||
<string name="native_d353a99eb4556847" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Dibatalkan"</string>
|
||||
<string name="native_d354ee936f59115e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"screen record"</string>
|
||||
<string name="native_d3974fd6d3b6c81f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Eksekusi Tertunda"</string>
|
||||
<string name="native_d3f75e24411053c4" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"AI pribadi di perangkat Anda"</string>
|
||||
@@ -1344,7 +1343,6 @@
|
||||
<string name="native_e89bdf093e8a6756" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Penyedia & Model"</string>
|
||||
<string name="native_e93372533f323b2f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Node"</string>
|
||||
<string name="native_e98feb46bd5d7792" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"%1$s "</string>
|
||||
<string name="native_e9e3a4b23345cb57" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Kedaluwarsa dalam %1$ss"</string>
|
||||
<string name="native_ea14eb224e89a810" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Perintah tidak tersedia"</string>
|
||||
<string name="native_ea2100dc89ae9fe2" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Log"</string>
|
||||
<string name="native_ea5cddf7a04d7b94" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Hubungkan Gateway untuk memeriksa proposal Skill Workshop."</string>
|
||||
|
||||
@@ -247,6 +247,7 @@
|
||||
<string name="native_2821f0168d6dd9df" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Trasforma questo dispositivo in un nodo OpenClaw sicuro per chat, voce, fotocamera e strumenti del dispositivo."</string>
|
||||
<string name="native_2831546121359362" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Configurazione manuale"</string>
|
||||
<string name="native_28bb849671cab958" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Suggerimento: interrompi l\'ascolto per inviare il turno acquisito."</string>
|
||||
<string name="native_28d03596d24eeb4e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Salta"</string>
|
||||
<string name="native_28d55ec6974d84e8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Richiesta vocale non riuscita"</string>
|
||||
<string name="native_29129d736e62995a" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Verrà rifiutata \"%1$s\" e lo stato di Skill Workshop verrà aggiornato dal gateway."</string>
|
||||
<string name="native_2937013f21818106" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"update"</string>
|
||||
@@ -924,7 +925,6 @@
|
||||
<string name="native_9d60841e0a7891df" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Controlla"</string>
|
||||
<string name="native_9d988c13c67e88e6" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Leggi la posizione di questo telefono"</string>
|
||||
<string name="native_9db0df0afb47b562" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Chiave della skill"</string>
|
||||
<string name="native_9dca3c2b01b40c0b" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Scade tra %1$sm %2$ss"</string>
|
||||
<string name="native_9e37aa0704a3cf44" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"%1$s installata."</string>
|
||||
<string name="native_9e4635769f0aaf0d" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway"</string>
|
||||
<string name="native_9e9bef85bb320b81" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Modalità di inoltro"</string>
|
||||
@@ -1221,7 +1221,6 @@
|
||||
<string name="native_d2ee46edf00c8eb6" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"... +%1$s altri"</string>
|
||||
<string name="native_d30f33731adb6b40" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Espandi l\'elenco di controllo del piano"</string>
|
||||
<string name="native_d31de1a5c5c8ba2a" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Browser"</string>
|
||||
<string name="native_d353a99eb4556847" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Annullato"</string>
|
||||
<string name="native_d354ee936f59115e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"screen record"</string>
|
||||
<string name="native_d3974fd6d3b6c81f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Esecuzione in sospeso"</string>
|
||||
<string name="native_d3f75e24411053c4" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"AI personale sui tuoi dispositivi"</string>
|
||||
@@ -1344,7 +1343,6 @@
|
||||
<string name="native_e89bdf093e8a6756" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Provider e modelli"</string>
|
||||
<string name="native_e93372533f323b2f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Nodo"</string>
|
||||
<string name="native_e98feb46bd5d7792" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"%1$s "</string>
|
||||
<string name="native_e9e3a4b23345cb57" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Scade tra %1$ss"</string>
|
||||
<string name="native_ea14eb224e89a810" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Prompt non disponibile"</string>
|
||||
<string name="native_ea2100dc89ae9fe2" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Log"</string>
|
||||
<string name="native_ea5cddf7a04d7b94" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Connetti il Gateway per esaminare le proposte di Skill Workshop."</string>
|
||||
|
||||
@@ -247,6 +247,7 @@
|
||||
<string name="native_2821f0168d6dd9df" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"このデバイスを、チャット、音声、カメラ、デバイスツール用の安全な OpenClaw ノードにします。"</string>
|
||||
<string name="native_2831546121359362" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"手動セットアップ"</string>
|
||||
<string name="native_28bb849671cab958" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"ヒント: 聞き取りを停止すると、キャプチャした発話が送信されます。"</string>
|
||||
<string name="native_28d03596d24eeb4e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"スキップ"</string>
|
||||
<string name="native_28d55ec6974d84e8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"音声リクエストに失敗しました"</string>
|
||||
<string name="native_29129d736e62995a" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"「%1$s」を却下し、GatewayからSkill Workshopの状態を更新します。"</string>
|
||||
<string name="native_2937013f21818106" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"update"</string>
|
||||
@@ -924,7 +925,6 @@
|
||||
<string name="native_9d60841e0a7891df" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"確認"</string>
|
||||
<string name="native_9d988c13c67e88e6" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"このスマートフォンの位置情報を読み取る"</string>
|
||||
<string name="native_9db0df0afb47b562" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Skillキー"</string>
|
||||
<string name="native_9dca3c2b01b40c0b" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"期限切れまで%1$sm %2$ss"</string>
|
||||
<string name="native_9e37aa0704a3cf44" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"%1$sをインストールしました。"</string>
|
||||
<string name="native_9e4635769f0aaf0d" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway"</string>
|
||||
<string name="native_9e9bef85bb320b81" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"転送モード"</string>
|
||||
@@ -1221,7 +1221,6 @@
|
||||
<string name="native_d2ee46edf00c8eb6" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"... +%1$s 件追加"</string>
|
||||
<string name="native_d30f33731adb6b40" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"計画チェックリストを展開"</string>
|
||||
<string name="native_d31de1a5c5c8ba2a" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"ブラウザ"</string>
|
||||
<string name="native_d353a99eb4556847" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"キャンセル済み"</string>
|
||||
<string name="native_d354ee936f59115e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"screen record"</string>
|
||||
<string name="native_d3974fd6d3b6c81f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"実行保留中"</string>
|
||||
<string name="native_d3f75e24411053c4" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"あなたのデバイス上のパーソナル AI"</string>
|
||||
@@ -1344,7 +1343,6 @@
|
||||
<string name="native_e89bdf093e8a6756" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"プロバイダーとモデル"</string>
|
||||
<string name="native_e93372533f323b2f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"ノード"</string>
|
||||
<string name="native_e98feb46bd5d7792" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"%1$s "</string>
|
||||
<string name="native_e9e3a4b23345cb57" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"期限切れまで%1$ss"</string>
|
||||
<string name="native_ea14eb224e89a810" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"プロンプトを利用できません"</string>
|
||||
<string name="native_ea2100dc89ae9fe2" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"ログ"</string>
|
||||
<string name="native_ea5cddf7a04d7b94" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Skill Workshopの提案を確認するには、Gatewayに接続してください。"</string>
|
||||
|
||||
@@ -247,6 +247,7 @@
|
||||
<string name="native_2821f0168d6dd9df" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"이 기기를 채팅, 음성, 카메라 및 기기 도구를 위한 안전한 OpenClaw 노드로 전환하세요."</string>
|
||||
<string name="native_2831546121359362" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"수동 설정"</string>
|
||||
<string name="native_28bb849671cab958" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"팁: 캡처된 턴을 보내려면 듣기를 중지하세요."</string>
|
||||
<string name="native_28d03596d24eeb4e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"건너뛰기"</string>
|
||||
<string name="native_28d55ec6974d84e8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"음성 요청 실패"</string>
|
||||
<string name="native_29129d736e62995a" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"이 작업은 \"%1$s\"을(를) 거부하고 Gateway에서 Skill Workshop 상태를 새로 고칩니다."</string>
|
||||
<string name="native_2937013f21818106" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"update"</string>
|
||||
@@ -924,7 +925,6 @@
|
||||
<string name="native_9d60841e0a7891df" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"확인"</string>
|
||||
<string name="native_9d988c13c67e88e6" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"이 휴대전화의 위치 확인"</string>
|
||||
<string name="native_9db0df0afb47b562" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Skill 키"</string>
|
||||
<string name="native_9dca3c2b01b40c0b" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"%1$s분 %2$s초 후 만료"</string>
|
||||
<string name="native_9e37aa0704a3cf44" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"%1$s을(를) 설치했습니다."</string>
|
||||
<string name="native_9e4635769f0aaf0d" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway"</string>
|
||||
<string name="native_9e9bef85bb320b81" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"전달 모드"</string>
|
||||
@@ -1221,7 +1221,6 @@
|
||||
<string name="native_d2ee46edf00c8eb6" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"... +%1$s개 더"</string>
|
||||
<string name="native_d30f33731adb6b40" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"계획 체크리스트 펼치기"</string>
|
||||
<string name="native_d31de1a5c5c8ba2a" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"브라우저"</string>
|
||||
<string name="native_d353a99eb4556847" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"취소됨"</string>
|
||||
<string name="native_d354ee936f59115e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"screen record"</string>
|
||||
<string name="native_d3974fd6d3b6c81f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"실행 대기 중"</string>
|
||||
<string name="native_d3f75e24411053c4" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"기기에서 사용하는 개인 AI"</string>
|
||||
@@ -1344,7 +1343,6 @@
|
||||
<string name="native_e89bdf093e8a6756" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"제공업체 및 모델"</string>
|
||||
<string name="native_e93372533f323b2f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"노드"</string>
|
||||
<string name="native_e98feb46bd5d7792" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"%1$s "</string>
|
||||
<string name="native_e9e3a4b23345cb57" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"%1$s초 후 만료"</string>
|
||||
<string name="native_ea14eb224e89a810" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"프롬프트를 사용할 수 없음"</string>
|
||||
<string name="native_ea2100dc89ae9fe2" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"로그"</string>
|
||||
<string name="native_ea5cddf7a04d7b94" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Skill Workshop 제안을 확인하려면 Gateway를 연결하세요."</string>
|
||||
|
||||
@@ -247,6 +247,7 @@
|
||||
<string name="native_2821f0168d6dd9df" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Verander dit apparaat in een beveiligd OpenClaw-knooppunt voor chat, spraak, camera en apparaathulpmiddelen."</string>
|
||||
<string name="native_2831546121359362" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Handmatige configuratie"</string>
|
||||
<string name="native_28bb849671cab958" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Tip: stop met luisteren om de vastgelegde beurt te verzenden."</string>
|
||||
<string name="native_28d03596d24eeb4e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Overslaan"</string>
|
||||
<string name="native_28d55ec6974d84e8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Spraakverzoek mislukt"</string>
|
||||
<string name="native_29129d736e62995a" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Hiermee wordt \"%1$s\" afgewezen en wordt de status van Skill Workshop vernieuwd vanuit de gateway."</string>
|
||||
<string name="native_2937013f21818106" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"update"</string>
|
||||
@@ -924,7 +925,6 @@
|
||||
<string name="native_9d60841e0a7891df" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Controleren"</string>
|
||||
<string name="native_9d988c13c67e88e6" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Locatie van deze telefoon lezen"</string>
|
||||
<string name="native_9db0df0afb47b562" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Skill-sleutel"</string>
|
||||
<string name="native_9dca3c2b01b40c0b" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Verloopt over %1$sm %2$ss"</string>
|
||||
<string name="native_9e37aa0704a3cf44" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"%1$s is geïnstalleerd."</string>
|
||||
<string name="native_9e4635769f0aaf0d" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateways"</string>
|
||||
<string name="native_9e9bef85bb320b81" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Doorstuurmodus"</string>
|
||||
@@ -1221,7 +1221,6 @@
|
||||
<string name="native_d2ee46edf00c8eb6" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"... +%1$s meer"</string>
|
||||
<string name="native_d30f33731adb6b40" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Checklist van plan uitklappen"</string>
|
||||
<string name="native_d31de1a5c5c8ba2a" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Browser"</string>
|
||||
<string name="native_d353a99eb4556847" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Geannuleerd"</string>
|
||||
<string name="native_d354ee936f59115e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"screen record"</string>
|
||||
<string name="native_d3974fd6d3b6c81f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Uitvoering in behandeling"</string>
|
||||
<string name="native_d3f75e24411053c4" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Persoonlijke AI op je apparaten"</string>
|
||||
@@ -1344,7 +1343,6 @@
|
||||
<string name="native_e89bdf093e8a6756" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Providers en modellen"</string>
|
||||
<string name="native_e93372533f323b2f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Knooppunt"</string>
|
||||
<string name="native_e98feb46bd5d7792" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"%1$s "</string>
|
||||
<string name="native_e9e3a4b23345cb57" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Verloopt over %1$ss"</string>
|
||||
<string name="native_ea14eb224e89a810" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Prompt niet beschikbaar"</string>
|
||||
<string name="native_ea2100dc89ae9fe2" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Logboeken"</string>
|
||||
<string name="native_ea5cddf7a04d7b94" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Verbind de Gateway om voorstellen van Skill Workshop te bekijken."</string>
|
||||
|
||||
@@ -247,6 +247,7 @@
|
||||
<string name="native_2821f0168d6dd9df" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Zmień to urządzenie w bezpieczny węzeł OpenClaw do czatu, głosu, kamery i narzędzi urządzenia."</string>
|
||||
<string name="native_2831546121359362" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Konfiguracja ręczna"</string>
|
||||
<string name="native_28bb849671cab958" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Wskazówka: zatrzymaj nasłuchiwanie, aby wysłać przechwyconą wypowiedź."</string>
|
||||
<string name="native_28d03596d24eeb4e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Pomiń"</string>
|
||||
<string name="native_28d55ec6974d84e8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Żądanie głosowe nie powiodło się"</string>
|
||||
<string name="native_29129d736e62995a" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Spowoduje to odrzucenie propozycji „%1$s” i odświeżenie stanu Skill Workshop z gateway."</string>
|
||||
<string name="native_2937013f21818106" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"update"</string>
|
||||
@@ -924,7 +925,6 @@
|
||||
<string name="native_9d60841e0a7891df" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Sprawdź"</string>
|
||||
<string name="native_9d988c13c67e88e6" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Odczytuj lokalizację tego telefonu"</string>
|
||||
<string name="native_9db0df0afb47b562" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Klucz Skill"</string>
|
||||
<string name="native_9dca3c2b01b40c0b" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Wygasa za %1$sm %2$ss"</string>
|
||||
<string name="native_9e37aa0704a3cf44" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Zainstalowano %1$s."</string>
|
||||
<string name="native_9e4635769f0aaf0d" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway"</string>
|
||||
<string name="native_9e9bef85bb320b81" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Tryb przekazywania"</string>
|
||||
@@ -1221,7 +1221,6 @@
|
||||
<string name="native_d2ee46edf00c8eb6" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"... +%1$s więcej"</string>
|
||||
<string name="native_d30f33731adb6b40" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Rozwiń listę kontrolną planu"</string>
|
||||
<string name="native_d31de1a5c5c8ba2a" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Przeglądarka"</string>
|
||||
<string name="native_d353a99eb4556847" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Anulowano"</string>
|
||||
<string name="native_d354ee936f59115e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"screen record"</string>
|
||||
<string name="native_d3974fd6d3b6c81f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Oczekujące uruchomienie"</string>
|
||||
<string name="native_d3f75e24411053c4" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Osobista AI na Twoich urządzeniach"</string>
|
||||
@@ -1344,7 +1343,6 @@
|
||||
<string name="native_e89bdf093e8a6756" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Dostawcy i modele"</string>
|
||||
<string name="native_e93372533f323b2f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Węzeł"</string>
|
||||
<string name="native_e98feb46bd5d7792" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"%1$s "</string>
|
||||
<string name="native_e9e3a4b23345cb57" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Wygasa za %1$ss"</string>
|
||||
<string name="native_ea14eb224e89a810" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Polecenie niedostępne"</string>
|
||||
<string name="native_ea2100dc89ae9fe2" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Dzienniki"</string>
|
||||
<string name="native_ea5cddf7a04d7b94" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Połącz Gateway, aby sprawdzić propozycje Skill Workshop."</string>
|
||||
|
||||
@@ -247,6 +247,7 @@
|
||||
<string name="native_2821f0168d6dd9df" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Transforme este dispositivo em um nó OpenClaw seguro para chat, voz, câmera e ferramentas do dispositivo."</string>
|
||||
<string name="native_2831546121359362" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Configuração manual"</string>
|
||||
<string name="native_28bb849671cab958" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Dica: pare de ouvir para enviar o turno capturado."</string>
|
||||
<string name="native_28d03596d24eeb4e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Pular"</string>
|
||||
<string name="native_28d55ec6974d84e8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Falha na solicitação de voz"</string>
|
||||
<string name="native_29129d736e62995a" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Isso rejeitará \"%1$s\" e atualizará o estado do Skill Workshop com base no gateway."</string>
|
||||
<string name="native_2937013f21818106" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"update"</string>
|
||||
@@ -924,7 +925,6 @@
|
||||
<string name="native_9d60841e0a7891df" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Verificar"</string>
|
||||
<string name="native_9d988c13c67e88e6" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Ler a localização deste telefone"</string>
|
||||
<string name="native_9db0df0afb47b562" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Chave da Skill"</string>
|
||||
<string name="native_9dca3c2b01b40c0b" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Expira em %1$sm %2$ss"</string>
|
||||
<string name="native_9e37aa0704a3cf44" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"%1$s foi instalada."</string>
|
||||
<string name="native_9e4635769f0aaf0d" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateways"</string>
|
||||
<string name="native_9e9bef85bb320b81" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Modo de encaminhamento"</string>
|
||||
@@ -1221,7 +1221,6 @@
|
||||
<string name="native_d2ee46edf00c8eb6" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"... +%1$s mais"</string>
|
||||
<string name="native_d30f33731adb6b40" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Expandir lista de verificação do plano"</string>
|
||||
<string name="native_d31de1a5c5c8ba2a" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Navegador"</string>
|
||||
<string name="native_d353a99eb4556847" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Cancelado"</string>
|
||||
<string name="native_d354ee936f59115e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"screen record"</string>
|
||||
<string name="native_d3974fd6d3b6c81f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Execução pendente"</string>
|
||||
<string name="native_d3f75e24411053c4" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"IA pessoal nos seus dispositivos"</string>
|
||||
@@ -1344,7 +1343,6 @@
|
||||
<string name="native_e89bdf093e8a6756" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Provedores e modelos"</string>
|
||||
<string name="native_e93372533f323b2f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Nó"</string>
|
||||
<string name="native_e98feb46bd5d7792" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"%1$s "</string>
|
||||
<string name="native_e9e3a4b23345cb57" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Expira em %1$ss"</string>
|
||||
<string name="native_ea14eb224e89a810" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Prompt indisponível"</string>
|
||||
<string name="native_ea2100dc89ae9fe2" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Logs"</string>
|
||||
<string name="native_ea5cddf7a04d7b94" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Conecte o Gateway para inspecionar as propostas do Skill Workshop."</string>
|
||||
|
||||
@@ -247,6 +247,7 @@
|
||||
<string name="native_2821f0168d6dd9df" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Превратите это устройство в безопасный узел OpenClaw для чата, голоса, камеры и инструментов устройства."</string>
|
||||
<string name="native_2831546121359362" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Ручная настройка"</string>
|
||||
<string name="native_28bb849671cab958" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Совет: остановите прослушивание, чтобы отправить записанную реплику."</string>
|
||||
<string name="native_28d03596d24eeb4e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Пропустить"</string>
|
||||
<string name="native_28d55ec6974d84e8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Не удалось выполнить голосовой запрос"</string>
|
||||
<string name="native_29129d736e62995a" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Предложение \"%1$s\" будет отклонено, а состояние Skill Workshop будет обновлено из Gateway."</string>
|
||||
<string name="native_2937013f21818106" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"update"</string>
|
||||
@@ -924,7 +925,6 @@
|
||||
<string name="native_9d60841e0a7891df" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Проверить"</string>
|
||||
<string name="native_9d988c13c67e88e6" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Получать местоположение этого телефона"</string>
|
||||
<string name="native_9db0df0afb47b562" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Ключ Skill"</string>
|
||||
<string name="native_9dca3c2b01b40c0b" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Истекает через %1$s мин %2$s с"</string>
|
||||
<string name="native_9e37aa0704a3cf44" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Установлен %1$s."</string>
|
||||
<string name="native_9e4635769f0aaf0d" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway"</string>
|
||||
<string name="native_9e9bef85bb320b81" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Режим пересылки"</string>
|
||||
@@ -1221,7 +1221,6 @@
|
||||
<string name="native_d2ee46edf00c8eb6" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"... +%1$s еще"</string>
|
||||
<string name="native_d30f33731adb6b40" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Развернуть контрольный список плана"</string>
|
||||
<string name="native_d31de1a5c5c8ba2a" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Браузер"</string>
|
||||
<string name="native_d353a99eb4556847" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Отменено"</string>
|
||||
<string name="native_d354ee936f59115e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"screen record"</string>
|
||||
<string name="native_d3974fd6d3b6c81f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Запуск ожидается"</string>
|
||||
<string name="native_d3f75e24411053c4" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Персональный ИИ на ваших устройствах"</string>
|
||||
@@ -1344,7 +1343,6 @@
|
||||
<string name="native_e89bdf093e8a6756" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Провайдеры и модели"</string>
|
||||
<string name="native_e93372533f323b2f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Узел"</string>
|
||||
<string name="native_e98feb46bd5d7792" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"%1$s "</string>
|
||||
<string name="native_e9e3a4b23345cb57" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Истекает через %1$s с"</string>
|
||||
<string name="native_ea14eb224e89a810" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Запрос недоступен"</string>
|
||||
<string name="native_ea2100dc89ae9fe2" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Журналы"</string>
|
||||
<string name="native_ea5cddf7a04d7b94" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Подключите Gateway, чтобы просмотреть предложения Skill Workshop."</string>
|
||||
|
||||
@@ -247,6 +247,7 @@
|
||||
<string name="native_2821f0168d6dd9df" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gör den här enheten till en säker OpenClaw-nod för chatt, röst, kamera och enhetsverktyg."</string>
|
||||
<string name="native_2831546121359362" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Manuell konfiguration"</string>
|
||||
<string name="native_28bb849671cab958" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Tips: sluta lyssna för att skicka den fångade turen."</string>
|
||||
<string name="native_28d03596d24eeb4e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Hoppa över"</string>
|
||||
<string name="native_28d55ec6974d84e8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Röstbegäran misslyckades"</string>
|
||||
<string name="native_29129d736e62995a" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Detta avvisar \"%1$s\" och uppdaterar statusen för Skill Workshop från Gateway."</string>
|
||||
<string name="native_2937013f21818106" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"update"</string>
|
||||
@@ -924,7 +925,6 @@
|
||||
<string name="native_9d60841e0a7891df" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Kontrollera"</string>
|
||||
<string name="native_9d988c13c67e88e6" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Läs av den här telefonens plats"</string>
|
||||
<string name="native_9db0df0afb47b562" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Skill-nyckel"</string>
|
||||
<string name="native_9dca3c2b01b40c0b" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Upphör om %1$sm %2$ss"</string>
|
||||
<string name="native_9e37aa0704a3cf44" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Installerade %1$s."</string>
|
||||
<string name="native_9e4635769f0aaf0d" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateways"</string>
|
||||
<string name="native_9e9bef85bb320b81" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Vidarebefordringsläge"</string>
|
||||
@@ -1165,7 +1165,7 @@
|
||||
<string name="native_c9aa0d6611abe5f0" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Ta bort automatisering"</string>
|
||||
<string name="native_c9e4f8f40d07909e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Standardassistent"</string>
|
||||
<string name="native_c9e9b24ded599d1b" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Välj en %1$s-leverantör som stöds på Gateway"</string>
|
||||
<string name="native_ca184496974204a0" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Otillgänglig"</string>
|
||||
<string name="native_ca184496974204a0" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Inte tillgänglig"</string>
|
||||
<string name="native_ca1fd4548524cb2e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Tom mapp"</string>
|
||||
<string name="native_ca7981b46ecf2c17" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Av"</string>
|
||||
<string name="native_cab94aba84f97f7f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Typografi"</string>
|
||||
@@ -1221,7 +1221,6 @@
|
||||
<string name="native_d2ee46edf00c8eb6" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"... +%1$s till"</string>
|
||||
<string name="native_d30f33731adb6b40" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Visa planens checklista"</string>
|
||||
<string name="native_d31de1a5c5c8ba2a" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Webbläsare"</string>
|
||||
<string name="native_d353a99eb4556847" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Avbruten"</string>
|
||||
<string name="native_d354ee936f59115e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"screen record"</string>
|
||||
<string name="native_d3974fd6d3b6c81f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Körning väntar"</string>
|
||||
<string name="native_d3f75e24411053c4" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Personlig AI på dina enheter"</string>
|
||||
@@ -1344,7 +1343,6 @@
|
||||
<string name="native_e89bdf093e8a6756" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Leverantörer och modeller"</string>
|
||||
<string name="native_e93372533f323b2f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Nod"</string>
|
||||
<string name="native_e98feb46bd5d7792" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"%1$s "</string>
|
||||
<string name="native_e9e3a4b23345cb57" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Upphör om %1$ss"</string>
|
||||
<string name="native_ea14eb224e89a810" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Prompten är inte tillgänglig"</string>
|
||||
<string name="native_ea2100dc89ae9fe2" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Loggar"</string>
|
||||
<string name="native_ea5cddf7a04d7b94" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Anslut Gateway för att granska förslag från Skill Workshop."</string>
|
||||
|
||||
@@ -247,6 +247,7 @@
|
||||
<string name="native_2821f0168d6dd9df" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"เปลี่ยนอุปกรณ์นี้ให้เป็นโหนด OpenClaw ที่ปลอดภัยสำหรับแชท, เสียง, กล้อง และเครื่องมืออุปกรณ์"</string>
|
||||
<string name="native_2831546121359362" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"ตั้งค่าด้วยตนเอง"</string>
|
||||
<string name="native_28bb849671cab958" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"เคล็ดลับ: หยุดฟังเพื่อส่งช่วงที่บันทึกไว้"</string>
|
||||
<string name="native_28d03596d24eeb4e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"ข้าม"</string>
|
||||
<string name="native_28d55ec6974d84e8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"คำขอด้วยเสียงล้มเหลว"</string>
|
||||
<string name="native_29129d736e62995a" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"การดำเนินการนี้จะปฏิเสธ \"%1$s\" และรีเฟรชสถานะ Skill Workshop จาก Gateway"</string>
|
||||
<string name="native_2937013f21818106" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"update"</string>
|
||||
@@ -924,7 +925,6 @@
|
||||
<string name="native_9d60841e0a7891df" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"ตรวจสอบ"</string>
|
||||
<string name="native_9d988c13c67e88e6" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"อ่านตำแหน่งของโทรศัพท์เครื่องนี้"</string>
|
||||
<string name="native_9db0df0afb47b562" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"คีย์ของ Skill"</string>
|
||||
<string name="native_9dca3c2b01b40c0b" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"หมดอายุใน %1$s นาที %2$s วินาที"</string>
|
||||
<string name="native_9e37aa0704a3cf44" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"ติดตั้ง %1$s แล้ว"</string>
|
||||
<string name="native_9e4635769f0aaf0d" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway"</string>
|
||||
<string name="native_9e9bef85bb320b81" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"โหมดการส่งต่อ"</string>
|
||||
@@ -1221,7 +1221,6 @@
|
||||
<string name="native_d2ee46edf00c8eb6" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"... +%1$s เพิ่มเติม"</string>
|
||||
<string name="native_d30f33731adb6b40" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"ขยายรายการตรวจสอบแผน"</string>
|
||||
<string name="native_d31de1a5c5c8ba2a" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"เบราว์เซอร์"</string>
|
||||
<string name="native_d353a99eb4556847" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"ยกเลิกแล้ว"</string>
|
||||
<string name="native_d354ee936f59115e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"screen record"</string>
|
||||
<string name="native_d3974fd6d3b6c81f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"รอการเรียกใช้"</string>
|
||||
<string name="native_d3f75e24411053c4" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"AI ส่วนตัวบนอุปกรณ์ของคุณ"</string>
|
||||
@@ -1344,7 +1343,6 @@
|
||||
<string name="native_e89bdf093e8a6756" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"ผู้ให้บริการและโมเดล"</string>
|
||||
<string name="native_e93372533f323b2f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"โหนด"</string>
|
||||
<string name="native_e98feb46bd5d7792" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"%1$s "</string>
|
||||
<string name="native_e9e3a4b23345cb57" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"หมดอายุใน %1$s วินาที"</string>
|
||||
<string name="native_ea14eb224e89a810" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"ไม่มีพรอมต์"</string>
|
||||
<string name="native_ea2100dc89ae9fe2" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"บันทึก"</string>
|
||||
<string name="native_ea5cddf7a04d7b94" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"เชื่อมต่อ Gateway เพื่อตรวจสอบข้อเสนอ Skill Workshop"</string>
|
||||
|
||||
@@ -247,6 +247,7 @@
|
||||
<string name="native_2821f0168d6dd9df" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Bu cihazı sohbet, ses, kamera ve cihaz araçları için güvenli bir OpenClaw düğümüne dönüştürün."</string>
|
||||
<string name="native_2831546121359362" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Manuel kurulum"</string>
|
||||
<string name="native_28bb849671cab958" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"İpucu: yakalanan sırayı göndermek için dinlemeyi durdurun."</string>
|
||||
<string name="native_28d03596d24eeb4e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Atla"</string>
|
||||
<string name="native_28d55ec6974d84e8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Sesli istek başarısız oldu"</string>
|
||||
<string name="native_29129d736e62995a" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Bu işlem \"%1$s\" teklifini reddedecek ve Skill Workshop durumunu Gateway\'den yenileyecek."</string>
|
||||
<string name="native_2937013f21818106" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"update"</string>
|
||||
@@ -924,7 +925,6 @@
|
||||
<string name="native_9d60841e0a7891df" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Kontrol et"</string>
|
||||
<string name="native_9d988c13c67e88e6" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Bu telefonun konumunu okuyun"</string>
|
||||
<string name="native_9db0df0afb47b562" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Skill Anahtarı"</string>
|
||||
<string name="native_9dca3c2b01b40c0b" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Sona ermesine %1$s dk %2$s sn"</string>
|
||||
<string name="native_9e37aa0704a3cf44" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"%1$s yüklendi."</string>
|
||||
<string name="native_9e4635769f0aaf0d" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway\'ler"</string>
|
||||
<string name="native_9e9bef85bb320b81" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Yönlendirme Modu"</string>
|
||||
@@ -1221,7 +1221,6 @@
|
||||
<string name="native_d2ee46edf00c8eb6" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"... +%1$s daha"</string>
|
||||
<string name="native_d30f33731adb6b40" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Plan kontrol listesini genişlet"</string>
|
||||
<string name="native_d31de1a5c5c8ba2a" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Tarayıcı"</string>
|
||||
<string name="native_d353a99eb4556847" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"İptal edildi"</string>
|
||||
<string name="native_d354ee936f59115e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"screen record"</string>
|
||||
<string name="native_d3974fd6d3b6c81f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Çalıştırma Bekliyor"</string>
|
||||
<string name="native_d3f75e24411053c4" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Cihazlarınızda kişisel AI"</string>
|
||||
@@ -1344,7 +1343,6 @@
|
||||
<string name="native_e89bdf093e8a6756" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Sağlayıcılar ve Modeller"</string>
|
||||
<string name="native_e93372533f323b2f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Düğüm"</string>
|
||||
<string name="native_e98feb46bd5d7792" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"%1$s "</string>
|
||||
<string name="native_e9e3a4b23345cb57" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Sona ermesine %1$s sn"</string>
|
||||
<string name="native_ea14eb224e89a810" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"İstem kullanılamıyor"</string>
|
||||
<string name="native_ea2100dc89ae9fe2" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Günlükler"</string>
|
||||
<string name="native_ea5cddf7a04d7b94" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Skill Workshop önerilerini incelemek için Gateway\'e bağlanın."</string>
|
||||
|
||||
@@ -247,6 +247,7 @@
|
||||
<string name="native_2821f0168d6dd9df" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Перетворіть цей пристрій на захищений вузол OpenClaw для чату, голосу, камери та інструментів пристрою."</string>
|
||||
<string name="native_2831546121359362" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Ручне налаштування"</string>
|
||||
<string name="native_28bb849671cab958" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Порада: зупиніть прослуховування, щоб надіслати записану репліку."</string>
|
||||
<string name="native_28d03596d24eeb4e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Пропустити"</string>
|
||||
<string name="native_28d55ec6974d84e8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Не вдалося виконати голосовий запит"</string>
|
||||
<string name="native_29129d736e62995a" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Буде відхилено \"%1$s\", а стан Майстерні навичок оновлено з Gateway."</string>
|
||||
<string name="native_2937013f21818106" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"update"</string>
|
||||
@@ -924,7 +925,6 @@
|
||||
<string name="native_9d60841e0a7891df" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Перевірити"</string>
|
||||
<string name="native_9d988c13c67e88e6" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Отримувати дані про місцезнаходження цього телефону"</string>
|
||||
<string name="native_9db0df0afb47b562" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Ключ навички"</string>
|
||||
<string name="native_9dca3c2b01b40c0b" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Залишилося %1$s хв %2$s с"</string>
|
||||
<string name="native_9e37aa0704a3cf44" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Установлено %1$s."</string>
|
||||
<string name="native_9e4635769f0aaf0d" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Шлюзи"</string>
|
||||
<string name="native_9e9bef85bb320b81" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Режим пересилання"</string>
|
||||
@@ -1221,7 +1221,6 @@
|
||||
<string name="native_d2ee46edf00c8eb6" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"... +%1$s ще"</string>
|
||||
<string name="native_d30f33731adb6b40" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Розгорнути контрольний список плану"</string>
|
||||
<string name="native_d31de1a5c5c8ba2a" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Браузер"</string>
|
||||
<string name="native_d353a99eb4556847" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Скасовано"</string>
|
||||
<string name="native_d354ee936f59115e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"screen record"</string>
|
||||
<string name="native_d3974fd6d3b6c81f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Очікує запуску"</string>
|
||||
<string name="native_d3f75e24411053c4" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Персональний ШІ на ваших пристроях"</string>
|
||||
@@ -1344,7 +1343,6 @@
|
||||
<string name="native_e89bdf093e8a6756" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Постачальники й моделі"</string>
|
||||
<string name="native_e93372533f323b2f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Вузол"</string>
|
||||
<string name="native_e98feb46bd5d7792" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"%1$s "</string>
|
||||
<string name="native_e9e3a4b23345cb57" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Залишилося %1$s с"</string>
|
||||
<string name="native_ea14eb224e89a810" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Запит недоступний"</string>
|
||||
<string name="native_ea2100dc89ae9fe2" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Журнали"</string>
|
||||
<string name="native_ea5cddf7a04d7b94" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Підключіть Gateway, щоб переглянути пропозиції Skill Workshop."</string>
|
||||
|
||||
@@ -247,6 +247,7 @@
|
||||
<string name="native_2821f0168d6dd9df" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Biến thiết bị này thành một nút OpenClaw an toàn cho trò chuyện, thoại, camera và công cụ thiết bị."</string>
|
||||
<string name="native_2831546121359362" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Thiết lập thủ công"</string>
|
||||
<string name="native_28bb849671cab958" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Mẹo: dừng nghe để gửi lượt đã ghi lại."</string>
|
||||
<string name="native_28d03596d24eeb4e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Bỏ qua"</string>
|
||||
<string name="native_28d55ec6974d84e8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Yêu cầu giọng nói không thành công"</string>
|
||||
<string name="native_29129d736e62995a" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Thao tác này sẽ từ chối \"%1$s\" và làm mới trạng thái Skill Workshop từ Gateway."</string>
|
||||
<string name="native_2937013f21818106" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"update"</string>
|
||||
@@ -924,7 +925,6 @@
|
||||
<string name="native_9d60841e0a7891df" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Kiểm tra"</string>
|
||||
<string name="native_9d988c13c67e88e6" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Đọc vị trí của điện thoại này"</string>
|
||||
<string name="native_9db0df0afb47b562" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Khóa Skill"</string>
|
||||
<string name="native_9dca3c2b01b40c0b" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Hết hạn sau %1$sm %2$ss"</string>
|
||||
<string name="native_9e37aa0704a3cf44" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Đã cài đặt %1$s."</string>
|
||||
<string name="native_9e4635769f0aaf0d" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway"</string>
|
||||
<string name="native_9e9bef85bb320b81" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Chế độ chuyển tiếp"</string>
|
||||
@@ -1221,7 +1221,6 @@
|
||||
<string name="native_d2ee46edf00c8eb6" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"... +%1$s nữa"</string>
|
||||
<string name="native_d30f33731adb6b40" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Mở rộng danh sách kiểm tra kế hoạch"</string>
|
||||
<string name="native_d31de1a5c5c8ba2a" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Trình duyệt"</string>
|
||||
<string name="native_d353a99eb4556847" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Đã hủy"</string>
|
||||
<string name="native_d354ee936f59115e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"screen record"</string>
|
||||
<string name="native_d3974fd6d3b6c81f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Lượt chạy đang chờ"</string>
|
||||
<string name="native_d3f75e24411053c4" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"AI cá nhân trên các thiết bị của bạn"</string>
|
||||
@@ -1344,7 +1343,6 @@
|
||||
<string name="native_e89bdf093e8a6756" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Nhà cung cấp & Mô hình"</string>
|
||||
<string name="native_e93372533f323b2f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Nút"</string>
|
||||
<string name="native_e98feb46bd5d7792" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"%1$s "</string>
|
||||
<string name="native_e9e3a4b23345cb57" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Hết hạn sau %1$ss"</string>
|
||||
<string name="native_ea14eb224e89a810" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Câu lệnh không khả dụng"</string>
|
||||
<string name="native_ea2100dc89ae9fe2" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Nhật ký"</string>
|
||||
<string name="native_ea5cddf7a04d7b94" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Kết nối Gateway để kiểm tra các đề xuất Skill Workshop."</string>
|
||||
|
||||
@@ -247,6 +247,7 @@
|
||||
<string name="native_2821f0168d6dd9df" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"将此设备变成安全的 OpenClaw 节点,用于聊天、语音、摄像头和设备工具。"</string>
|
||||
<string name="native_2831546121359362" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"手动设置"</string>
|
||||
<string name="native_28bb849671cab958" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"提示:停止聆听即可发送已捕获的发言。"</string>
|
||||
<string name="native_28d03596d24eeb4e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"跳过"</string>
|
||||
<string name="native_28d55ec6974d84e8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"语音请求失败"</string>
|
||||
<string name="native_29129d736e62995a" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"这将拒绝“%1$s”,并从 Gateway 刷新 Skill Workshop 状态。"</string>
|
||||
<string name="native_2937013f21818106" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"update"</string>
|
||||
@@ -924,7 +925,6 @@
|
||||
<string name="native_9d60841e0a7891df" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"检查"</string>
|
||||
<string name="native_9d988c13c67e88e6" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"读取此手机的位置信息"</string>
|
||||
<string name="native_9db0df0afb47b562" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Skill 键"</string>
|
||||
<string name="native_9dca3c2b01b40c0b" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"%1$s分%2$s秒后过期"</string>
|
||||
<string name="native_9e37aa0704a3cf44" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"已安装 %1$s。"</string>
|
||||
<string name="native_9e4635769f0aaf0d" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateways"</string>
|
||||
<string name="native_9e9bef85bb320b81" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"转发模式"</string>
|
||||
@@ -1221,7 +1221,6 @@
|
||||
<string name="native_d2ee46edf00c8eb6" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"... +%1$s 个更多"</string>
|
||||
<string name="native_d30f33731adb6b40" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"展开计划清单"</string>
|
||||
<string name="native_d31de1a5c5c8ba2a" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"浏览器"</string>
|
||||
<string name="native_d353a99eb4556847" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"已取消"</string>
|
||||
<string name="native_d354ee936f59115e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"screen record"</string>
|
||||
<string name="native_d3974fd6d3b6c81f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"运行待处理项"</string>
|
||||
<string name="native_d3f75e24411053c4" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"您设备上的个人 AI"</string>
|
||||
@@ -1344,7 +1343,6 @@
|
||||
<string name="native_e89bdf093e8a6756" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"提供商与模型"</string>
|
||||
<string name="native_e93372533f323b2f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"节点"</string>
|
||||
<string name="native_e98feb46bd5d7792" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"%1$s "</string>
|
||||
<string name="native_e9e3a4b23345cb57" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"%1$s秒后过期"</string>
|
||||
<string name="native_ea14eb224e89a810" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"提示词不可用"</string>
|
||||
<string name="native_ea2100dc89ae9fe2" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"日志"</string>
|
||||
<string name="native_ea5cddf7a04d7b94" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"连接 Gateway 以检查 Skill Workshop 提案。"</string>
|
||||
|
||||
@@ -247,6 +247,7 @@
|
||||
<string name="native_2821f0168d6dd9df" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"將此裝置變成安全的 OpenClaw 節點,用於聊天、語音、相機與裝置工具。"</string>
|
||||
<string name="native_2831546121359362" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"手動設定"</string>
|
||||
<string name="native_28bb849671cab958" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"提示:停止聆聽即可傳送擷取到的發言。"</string>
|
||||
<string name="native_28d03596d24eeb4e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"略過"</string>
|
||||
<string name="native_28d55ec6974d84e8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"語音請求失敗"</string>
|
||||
<string name="native_29129d736e62995a" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"這將拒絕「%1$s」,並從 Gateway 重新整理 Skill Workshop 狀態。"</string>
|
||||
<string name="native_2937013f21818106" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"update"</string>
|
||||
@@ -924,7 +925,6 @@
|
||||
<string name="native_9d60841e0a7891df" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"檢查"</string>
|
||||
<string name="native_9d988c13c67e88e6" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"讀取此手機的位置"</string>
|
||||
<string name="native_9db0df0afb47b562" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Skill 金鑰"</string>
|
||||
<string name="native_9dca3c2b01b40c0b" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"將於 %1$sm %2$ss 後過期"</string>
|
||||
<string name="native_9e37aa0704a3cf44" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"已安裝 %1$s。"</string>
|
||||
<string name="native_9e4635769f0aaf0d" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateways"</string>
|
||||
<string name="native_9e9bef85bb320b81" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"轉發模式"</string>
|
||||
@@ -1221,7 +1221,6 @@
|
||||
<string name="native_d2ee46edf00c8eb6" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"... 另有 %1$s 個"</string>
|
||||
<string name="native_d30f33731adb6b40" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"展開計畫檢查清單"</string>
|
||||
<string name="native_d31de1a5c5c8ba2a" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"瀏覽器"</string>
|
||||
<string name="native_d353a99eb4556847" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"已取消"</string>
|
||||
<string name="native_d354ee936f59115e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"screen record"</string>
|
||||
<string name="native_d3974fd6d3b6c81f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"執行待處理項目"</string>
|
||||
<string name="native_d3f75e24411053c4" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"您裝置上的個人 AI"</string>
|
||||
@@ -1344,7 +1343,6 @@
|
||||
<string name="native_e89bdf093e8a6756" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"提供者與模型"</string>
|
||||
<string name="native_e93372533f323b2f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"節點"</string>
|
||||
<string name="native_e98feb46bd5d7792" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"%1$s "</string>
|
||||
<string name="native_e9e3a4b23345cb57" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"將於 %1$ss 後過期"</string>
|
||||
<string name="native_ea14eb224e89a810" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"提示詞無法使用"</string>
|
||||
<string name="native_ea2100dc89ae9fe2" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"記錄"</string>
|
||||
<string name="native_ea5cddf7a04d7b94" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"請連線至 Gateway 以檢查 Skill Workshop 提案。"</string>
|
||||
|
||||
@@ -247,6 +247,7 @@
|
||||
<string name="native_2821f0168d6dd9df" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Turn this device into a secure OpenClaw node for chat, voice, camera, and device tools."</string>
|
||||
<string name="native_2831546121359362" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Manual setup"</string>
|
||||
<string name="native_28bb849671cab958" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Tip: stop listening to send the captured turn."</string>
|
||||
<string name="native_28d03596d24eeb4e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Skip"</string>
|
||||
<string name="native_28d55ec6974d84e8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Voice request failed"</string>
|
||||
<string name="native_29129d736e62995a" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"This will reject \"%1$s\" and refresh Skill Workshop state from the gateway."</string>
|
||||
<string name="native_2937013f21818106" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"update"</string>
|
||||
@@ -924,7 +925,6 @@
|
||||
<string name="native_9d60841e0a7891df" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Check"</string>
|
||||
<string name="native_9d988c13c67e88e6" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Read this phone\'s location"</string>
|
||||
<string name="native_9db0df0afb47b562" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Skill Key"</string>
|
||||
<string name="native_9dca3c2b01b40c0b" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Expires in %1$sm %2$ss"</string>
|
||||
<string name="native_9e37aa0704a3cf44" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Installed %1$s."</string>
|
||||
<string name="native_9e4635769f0aaf0d" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateways"</string>
|
||||
<string name="native_9e9bef85bb320b81" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Forwarding Mode"</string>
|
||||
@@ -1221,7 +1221,6 @@
|
||||
<string name="native_d2ee46edf00c8eb6" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"... +%1$s more"</string>
|
||||
<string name="native_d30f33731adb6b40" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Expand plan checklist"</string>
|
||||
<string name="native_d31de1a5c5c8ba2a" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Browser"</string>
|
||||
<string name="native_d353a99eb4556847" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Cancelled"</string>
|
||||
<string name="native_d354ee936f59115e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"screen record"</string>
|
||||
<string name="native_d3974fd6d3b6c81f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Run Pending"</string>
|
||||
<string name="native_d3f75e24411053c4" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Personal AI on your devices"</string>
|
||||
@@ -1344,7 +1343,6 @@
|
||||
<string name="native_e89bdf093e8a6756" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Providers & Models"</string>
|
||||
<string name="native_e93372533f323b2f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Node"</string>
|
||||
<string name="native_e98feb46bd5d7792" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"%1$s "</string>
|
||||
<string name="native_e9e3a4b23345cb57" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Expires in %1$ss"</string>
|
||||
<string name="native_ea14eb224e89a810" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Prompt unavailable"</string>
|
||||
<string name="native_ea2100dc89ae9fe2" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Logs"</string>
|
||||
<string name="native_ea5cddf7a04d7b94" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Connect the gateway to inspect Skill Workshop proposals."</string>
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
package ai.openclaw.app.chat
|
||||
|
||||
import ai.openclaw.app.gateway.GatewayConnectErrorDetails
|
||||
import ai.openclaw.app.gateway.GatewayRequestRejected
|
||||
import ai.openclaw.app.gateway.GatewaySession
|
||||
import ai.openclaw.app.gateway.Question
|
||||
import ai.openclaw.app.gateway.QuestionAnswers
|
||||
import ai.openclaw.app.gateway.QuestionAnswersAnswersValue
|
||||
import ai.openclaw.app.gateway.QuestionGetResult
|
||||
import ai.openclaw.app.gateway.QuestionListResult
|
||||
import ai.openclaw.app.gateway.QuestionOption
|
||||
import ai.openclaw.app.gateway.QuestionRecord
|
||||
import ai.openclaw.app.ui.chat.questionCountdown
|
||||
import ai.openclaw.app.ui.chat.terminalQuestionAnswer
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.advanceTimeBy
|
||||
@@ -12,6 +20,8 @@ import kotlinx.coroutines.test.runCurrent
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
@@ -54,15 +64,42 @@ class ChatQuestionTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun terminalPromptRetentionMatchesGatewayGrace() {
|
||||
fun terminalPromptsRemainInTheTimeline() {
|
||||
val prompt =
|
||||
ChatQuestionPrompt(
|
||||
record = record(status = "answered"),
|
||||
terminalObservedAtMs = 1_000,
|
||||
)
|
||||
|
||||
assertTrue(prompt.shouldRetainAfterList(15_999))
|
||||
assertFalse(prompt.shouldRetainAfterList(16_000))
|
||||
assertEquals(ChatQuestionStatus.AnsweredElsewhere, prompt.status(nowMs = Long.MAX_VALUE))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun countdownMatchesWebMinuteSecondFormat() {
|
||||
assertEquals("1:05", questionCountdown(expiresAtMs = 65_000, nowMs = 0))
|
||||
assertEquals("0:05", questionCountdown(expiresAtMs = 4_001, nowMs = 0))
|
||||
assertEquals("0:00", questionCountdown(expiresAtMs = 1_000, nowMs = 2_000))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun terminalSummaryUsesAnswersAndStatusLabels() {
|
||||
val answered =
|
||||
ChatQuestionPrompt(
|
||||
record =
|
||||
record(status = "answered").copy(
|
||||
answers = QuestionAnswers(mapOf("meal" to QuestionAnswersAnswersValue(listOf("Pizza", "Salad")))),
|
||||
),
|
||||
answeredLocally = true,
|
||||
)
|
||||
|
||||
assertEquals("Pizza, Salad", terminalQuestionAnswer(answered, question, ChatQuestionStatus.Answered))
|
||||
assertEquals("Skipped", terminalQuestionAnswer(answered, question, ChatQuestionStatus.Cancelled))
|
||||
assertEquals("Expired", terminalQuestionAnswer(answered, question, ChatQuestionStatus.Expired))
|
||||
assertEquals("Unavailable", terminalQuestionAnswer(answered, question, ChatQuestionStatus.Unavailable))
|
||||
assertEquals(
|
||||
"Answered elsewhere",
|
||||
terminalQuestionAnswer(answered.copy(record = answered.record.copy(answers = null)), question, ChatQuestionStatus.AnsweredElsewhere),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -162,6 +199,54 @@ class ChatQuestionTest {
|
||||
advanceUntilIdle()
|
||||
}
|
||||
|
||||
@Test
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun replayedPendingEventCannotReopenResolvedQuestion() =
|
||||
runTest {
|
||||
val json = Json { ignoreUnknownKeys = true }
|
||||
val pending = record(expiresAtMs = Long.MAX_VALUE)
|
||||
val controller = ChatController(scope = this, json = json, requestGateway = { _, _ -> "{}" })
|
||||
|
||||
controller.handleGatewayEvent("question.requested", json.encodeToString(pending))
|
||||
controller.handleGatewayEvent("question.resolved", """{"id":"ask_123","status":"answered"}""")
|
||||
controller.handleGatewayEvent("question.requested", json.encodeToString(pending))
|
||||
|
||||
assertEquals(
|
||||
ChatQuestionStatus.AnsweredElsewhere,
|
||||
controller.questions.value
|
||||
.single()
|
||||
.status(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun pendingListRecordCannotReopenResolvedQuestion() =
|
||||
runTest {
|
||||
val json = Json { ignoreUnknownKeys = true }
|
||||
val pending = record(expiresAtMs = Long.MAX_VALUE)
|
||||
val controller =
|
||||
ChatController(
|
||||
scope = this,
|
||||
json = json,
|
||||
requestGateway = { method, _ ->
|
||||
if (method == "question.list") json.encodeToString(QuestionListResult(listOf(pending))) else "{}"
|
||||
},
|
||||
)
|
||||
|
||||
controller.handleGatewayEvent("question.requested", json.encodeToString(pending))
|
||||
controller.handleGatewayEvent("question.resolved", """{"id":"ask_123","status":"cancelled"}""")
|
||||
controller.handleGatewayEvent("health", null)
|
||||
advanceUntilIdle()
|
||||
|
||||
assertEquals(
|
||||
ChatQuestionStatus.Cancelled,
|
||||
controller.questions.value
|
||||
.single()
|
||||
.status(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun resolvedEventReconcilesAfterDiscardingOlderList() =
|
||||
@@ -205,7 +290,7 @@ class ChatQuestionTest {
|
||||
|
||||
@Test
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun questionListRetainsRecentlyResolvedCard() =
|
||||
fun questionListRetainsResolvedSummaryPermanently() =
|
||||
runTest {
|
||||
val json = Json { ignoreUnknownKeys = true }
|
||||
val pending = record(id = "ask_done", expiresAtMs = Long.MAX_VALUE)
|
||||
@@ -231,25 +316,694 @@ class ChatQuestionTest {
|
||||
.status(),
|
||||
)
|
||||
|
||||
advanceTimeBy(QUESTION_TERMINAL_RETENTION_MS)
|
||||
advanceTimeBy(60_000)
|
||||
runCurrent()
|
||||
|
||||
assertTrue(controller.questions.value.isEmpty())
|
||||
assertEquals(listOf("ask_done"), controller.questions.value.map { it.record.id })
|
||||
}
|
||||
|
||||
@Test
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun locallyExpiredQuestionIsEvictedAfterTerminalGrace() =
|
||||
fun locallyExpiredQuestionRemainsAsSummary() =
|
||||
runTest {
|
||||
val json = Json { ignoreUnknownKeys = true }
|
||||
val controller = ChatController(scope = this, json = json, requestGateway = { _, _ -> "{}" })
|
||||
val pending = record(expiresAtMs = 1_000)
|
||||
|
||||
controller.handleGatewayEvent("question.requested", json.encodeToString(pending))
|
||||
advanceTimeBy(QUESTION_TERMINAL_RETENTION_MS + 1_001)
|
||||
advanceUntilIdle()
|
||||
|
||||
assertEquals(
|
||||
ChatQuestionStatus.Expired,
|
||||
controller.questions.value
|
||||
.single()
|
||||
.status(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun localExpiryReconcilesMissedRemoteAnswer() =
|
||||
runTest {
|
||||
val json = Json { ignoreUnknownKeys = true }
|
||||
val pending = record(expiresAtMs = System.currentTimeMillis() + 1_000)
|
||||
val answered = pending.copy(status = "answered")
|
||||
var listCalls = 0
|
||||
var getCalls = 0
|
||||
val controller =
|
||||
ChatController(
|
||||
scope = this,
|
||||
json = json,
|
||||
requestGateway = { method, _ ->
|
||||
when (method) {
|
||||
"question.list" -> {
|
||||
listCalls += 1
|
||||
json.encodeToString(QuestionListResult(if (listCalls == 1) listOf(pending) else emptyList()))
|
||||
}
|
||||
"question.get" -> {
|
||||
getCalls += 1
|
||||
json.encodeToString(QuestionGetResult(answered))
|
||||
}
|
||||
else -> "{}"
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
controller.handleGatewayEvent("question.requested", json.encodeToString(pending))
|
||||
runCurrent()
|
||||
advanceTimeBy(2_000)
|
||||
advanceUntilIdle()
|
||||
|
||||
assertEquals(2, listCalls)
|
||||
assertEquals(1, getCalls)
|
||||
assertEquals(
|
||||
ChatQuestionStatus.AnsweredElsewhere,
|
||||
controller.questions.value
|
||||
.single()
|
||||
.status(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun missingPendingQuestionUsesPerIdGetFallback() =
|
||||
runTest {
|
||||
val json = Json { ignoreUnknownKeys = true }
|
||||
val pending = record(expiresAtMs = Long.MAX_VALUE)
|
||||
val answered =
|
||||
pending.copy(
|
||||
status = "answered",
|
||||
answers = QuestionAnswers(mapOf("meal" to QuestionAnswersAnswersValue(listOf("Tacos")))),
|
||||
)
|
||||
var getParams: String? = null
|
||||
val controller =
|
||||
ChatController(
|
||||
scope = this,
|
||||
json = json,
|
||||
requestGateway = { method, params ->
|
||||
when (method) {
|
||||
"question.list" -> json.encodeToString(QuestionListResult(emptyList()))
|
||||
"question.get" -> {
|
||||
getParams = params
|
||||
json.encodeToString(QuestionGetResult(answered))
|
||||
}
|
||||
else -> "{}"
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
controller.handleGatewayEvent("question.requested", json.encodeToString(pending))
|
||||
advanceUntilIdle()
|
||||
|
||||
assertEquals(
|
||||
"ask_123",
|
||||
json
|
||||
.parseToJsonElement(checkNotNull(getParams))
|
||||
.jsonObject["id"]
|
||||
?.jsonPrimitive
|
||||
?.content,
|
||||
)
|
||||
assertEquals(
|
||||
listOf("Tacos"),
|
||||
controller.questions.value
|
||||
.single()
|
||||
.record.answers
|
||||
?.answers
|
||||
?.get("meal")
|
||||
?.answers,
|
||||
)
|
||||
assertEquals(
|
||||
ChatQuestionStatus.AnsweredElsewhere,
|
||||
controller.questions.value
|
||||
.single()
|
||||
.status(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun successfulRefreshRecordsApplyWhenAnotherFallbackFails() =
|
||||
runTest {
|
||||
val json = Json { ignoreUnknownKeys = true }
|
||||
val listedPending = record(id = "ask_listed")
|
||||
val recoveredPending = record(id = "ask_recovered")
|
||||
val failingPending = record(id = "ask_failing")
|
||||
val newlyMissingPending = record(id = "ask_newly_missing")
|
||||
val listedAnswered =
|
||||
listedPending.copy(
|
||||
status = "answered",
|
||||
answers = QuestionAnswers(mapOf("meal" to QuestionAnswersAnswersValue(listOf("Tacos")))),
|
||||
)
|
||||
val recoveredAnswered = recoveredPending.copy(status = "answered")
|
||||
val failingAnswered = failingPending.copy(status = "answered")
|
||||
val newlyMissingAnswered = newlyMissingPending.copy(status = "answered")
|
||||
val getCalls = mutableMapOf<String, Int>()
|
||||
var fallbackFailed = false
|
||||
val controller =
|
||||
ChatController(
|
||||
scope = this,
|
||||
json = json,
|
||||
requestGateway = { method, params ->
|
||||
when (method) {
|
||||
"question.list" -> {
|
||||
val records =
|
||||
if (!fallbackFailed) {
|
||||
listOf(listedAnswered, newlyMissingPending)
|
||||
} else {
|
||||
listOf(listedAnswered)
|
||||
}
|
||||
json.encodeToString(QuestionListResult(records))
|
||||
}
|
||||
"question.get" -> {
|
||||
val id =
|
||||
json
|
||||
.parseToJsonElement(checkNotNull(params))
|
||||
.jsonObject
|
||||
.getValue("id")
|
||||
.jsonPrimitive
|
||||
.content
|
||||
getCalls[id] = getCalls.getOrDefault(id, 0) + 1
|
||||
when (id) {
|
||||
recoveredPending.id ->
|
||||
json.encodeToString(
|
||||
QuestionGetResult(
|
||||
if (getCalls.getValue(id) == 1) recoveredPending else recoveredAnswered,
|
||||
),
|
||||
)
|
||||
newlyMissingPending.id -> json.encodeToString(QuestionGetResult(newlyMissingAnswered))
|
||||
failingPending.id -> {
|
||||
if (getCalls.getValue(id) == 1) {
|
||||
fallbackFailed = true
|
||||
error("temporary question.get failure")
|
||||
}
|
||||
json.encodeToString(QuestionGetResult(failingAnswered))
|
||||
}
|
||||
else -> error("unexpected question id")
|
||||
}
|
||||
}
|
||||
else -> "{}"
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
controller.handleGatewayEvent("question.requested", json.encodeToString(listedPending))
|
||||
controller.handleGatewayEvent("question.requested", json.encodeToString(recoveredPending))
|
||||
controller.handleGatewayEvent("question.requested", json.encodeToString(failingPending))
|
||||
controller.handleGatewayEvent("question.requested", json.encodeToString(newlyMissingPending))
|
||||
runCurrent()
|
||||
|
||||
assertTrue(controller.questions.value.isEmpty())
|
||||
val prompts = controller.questions.value.associateBy { it.record.id }
|
||||
assertEquals(ChatQuestionStatus.AnsweredElsewhere, prompts.getValue("ask_listed").status())
|
||||
assertEquals(
|
||||
listOf("Tacos"),
|
||||
prompts
|
||||
.getValue("ask_listed")
|
||||
.record
|
||||
.answers
|
||||
?.answers
|
||||
?.get("meal")
|
||||
?.answers,
|
||||
)
|
||||
assertEquals(ChatQuestionStatus.Pending, prompts.getValue("ask_recovered").status())
|
||||
assertEquals(ChatQuestionStatus.Pending, prompts.getValue("ask_failing").status())
|
||||
assertEquals(ChatQuestionStatus.Pending, prompts.getValue("ask_newly_missing").status())
|
||||
|
||||
advanceTimeBy(1_000)
|
||||
runCurrent()
|
||||
assertEquals(2, getCalls["ask_recovered"])
|
||||
assertEquals(2, getCalls["ask_failing"])
|
||||
assertEquals(1, getCalls["ask_newly_missing"])
|
||||
assertEquals(
|
||||
ChatQuestionStatus.AnsweredElsewhere,
|
||||
controller.questions.value
|
||||
.single { it.record.id == "ask_newly_missing" }
|
||||
.status(),
|
||||
)
|
||||
assertEquals(
|
||||
ChatQuestionStatus.AnsweredElsewhere,
|
||||
controller.questions.value
|
||||
.single { it.record.id == "ask_recovered" }
|
||||
.status(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun questionGetRetryResetsExhaustedBudgetAfterAnotherQuestionChanges() =
|
||||
runTest {
|
||||
val json = Json { ignoreUnknownKeys = true }
|
||||
val recovering = record(id = "ask_recovering")
|
||||
val unrelated = record(id = "ask_unrelated")
|
||||
val recovered = recovering.copy(status = "answered")
|
||||
var getCalls = 0
|
||||
val finalGetStarted = CompletableDeferred<Unit>()
|
||||
val releaseFinalGet = CompletableDeferred<Unit>()
|
||||
val controller =
|
||||
ChatController(
|
||||
scope = this,
|
||||
json = json,
|
||||
requestGateway = { method, _ ->
|
||||
when (method) {
|
||||
"question.list" -> json.encodeToString(QuestionListResult(listOf(unrelated)))
|
||||
"question.get" -> {
|
||||
getCalls += 1
|
||||
if (getCalls < 4) error("temporary question.get failure")
|
||||
if (getCalls == 4) {
|
||||
finalGetStarted.complete(Unit)
|
||||
releaseFinalGet.await()
|
||||
}
|
||||
json.encodeToString(QuestionGetResult(recovered))
|
||||
}
|
||||
"question.resolve" -> "{}"
|
||||
else -> "{}"
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
controller.handleGatewayEvent("question.requested", json.encodeToString(recovering))
|
||||
controller.handleGatewayEvent("question.requested", json.encodeToString(unrelated))
|
||||
runCurrent()
|
||||
assertEquals(1, getCalls)
|
||||
|
||||
advanceTimeBy(1_000)
|
||||
runCurrent()
|
||||
advanceTimeBy(2_000)
|
||||
runCurrent()
|
||||
advanceTimeBy(4_000)
|
||||
runCurrent()
|
||||
finalGetStarted.await()
|
||||
assertEquals(4, getCalls)
|
||||
|
||||
controller.skipQuestion(unrelated.id)
|
||||
runCurrent()
|
||||
releaseFinalGet.complete(Unit)
|
||||
runCurrent()
|
||||
advanceTimeBy(1_000)
|
||||
runCurrent()
|
||||
|
||||
assertEquals(5, getCalls)
|
||||
assertEquals(
|
||||
ChatQuestionStatus.AnsweredElsewhere,
|
||||
controller.questions.value
|
||||
.single { it.record.id == recovering.id }
|
||||
.status(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun questionGetRetryResetsBudgetWhenRevisionChangesDuringFinalBackoff() =
|
||||
runTest {
|
||||
val json = Json { ignoreUnknownKeys = true }
|
||||
val recovering = record(id = "ask_recovering")
|
||||
val unrelated = record(id = "ask_unrelated")
|
||||
val recovered = recovering.copy(status = "answered")
|
||||
var getCalls = 0
|
||||
val controller =
|
||||
ChatController(
|
||||
scope = this,
|
||||
json = json,
|
||||
requestGateway = { method, _ ->
|
||||
when (method) {
|
||||
"question.list" -> json.encodeToString(QuestionListResult(listOf(unrelated)))
|
||||
"question.get" -> {
|
||||
getCalls += 1
|
||||
if (getCalls < 5) error("temporary question.get failure")
|
||||
json.encodeToString(QuestionGetResult(recovered))
|
||||
}
|
||||
"question.resolve" -> "{}"
|
||||
else -> "{}"
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
controller.handleGatewayEvent("question.requested", json.encodeToString(recovering))
|
||||
controller.handleGatewayEvent("question.requested", json.encodeToString(unrelated))
|
||||
runCurrent()
|
||||
advanceTimeBy(1_000)
|
||||
runCurrent()
|
||||
advanceTimeBy(2_000)
|
||||
runCurrent()
|
||||
assertEquals(3, getCalls)
|
||||
|
||||
controller.skipQuestion(unrelated.id)
|
||||
runCurrent()
|
||||
advanceTimeBy(4_000)
|
||||
runCurrent()
|
||||
assertEquals(4, getCalls)
|
||||
advanceTimeBy(1_000)
|
||||
runCurrent()
|
||||
|
||||
assertEquals(5, getCalls)
|
||||
assertEquals(
|
||||
ChatQuestionStatus.AnsweredElsewhere,
|
||||
controller.questions.value
|
||||
.single { it.record.id == recovering.id }
|
||||
.status(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun locallyExpiredMissingQuestionUsesPerIdGetFallback() =
|
||||
runTest {
|
||||
val json = Json { ignoreUnknownKeys = true }
|
||||
val pending = record(expiresAtMs = 0)
|
||||
val answered =
|
||||
pending.copy(
|
||||
status = "answered",
|
||||
answers = QuestionAnswers(mapOf("meal" to QuestionAnswersAnswersValue(listOf("Tacos")))),
|
||||
)
|
||||
var getCalls = 0
|
||||
val controller =
|
||||
ChatController(
|
||||
scope = this,
|
||||
json = json,
|
||||
requestGateway = { method, _ ->
|
||||
when (method) {
|
||||
"question.list" -> json.encodeToString(QuestionListResult(emptyList()))
|
||||
"question.get" -> {
|
||||
getCalls += 1
|
||||
json.encodeToString(QuestionGetResult(answered))
|
||||
}
|
||||
else -> "{}"
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
controller.handleGatewayEvent("question.requested", json.encodeToString(pending))
|
||||
runCurrent()
|
||||
|
||||
assertEquals(1, getCalls)
|
||||
assertEquals(
|
||||
listOf("Tacos"),
|
||||
controller.questions.value
|
||||
.single()
|
||||
.record.answers
|
||||
?.answers
|
||||
?.get("meal")
|
||||
?.answers,
|
||||
)
|
||||
assertEquals(
|
||||
ChatQuestionStatus.AnsweredElsewhere,
|
||||
controller.questions.value
|
||||
.single()
|
||||
.status(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun missingQuestionGetRetriesAfterOneTwoAndFourSeconds() =
|
||||
runTest {
|
||||
val json = Json { ignoreUnknownKeys = true }
|
||||
val pending = record(expiresAtMs = Long.MAX_VALUE)
|
||||
var getCalls = 0
|
||||
val controller =
|
||||
ChatController(
|
||||
scope = this,
|
||||
json = json,
|
||||
requestGateway = { method, _ ->
|
||||
when (method) {
|
||||
"question.list" -> json.encodeToString(QuestionListResult(emptyList()))
|
||||
"question.get" -> {
|
||||
getCalls += 1
|
||||
if (getCalls < 4) error("temporary question.get failure")
|
||||
json.encodeToString(QuestionGetResult(pending))
|
||||
}
|
||||
else -> "{}"
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
controller.handleGatewayEvent("question.requested", json.encodeToString(pending))
|
||||
runCurrent()
|
||||
assertEquals(1, getCalls)
|
||||
advanceTimeBy(999)
|
||||
runCurrent()
|
||||
assertEquals(1, getCalls)
|
||||
advanceTimeBy(1)
|
||||
runCurrent()
|
||||
assertEquals(2, getCalls)
|
||||
advanceTimeBy(2_000)
|
||||
runCurrent()
|
||||
assertEquals(3, getCalls)
|
||||
advanceTimeBy(4_000)
|
||||
runCurrent()
|
||||
assertEquals(4, getCalls)
|
||||
}
|
||||
|
||||
@Test
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun missingQuestionNotFoundHasUnknownTerminalOutcome() =
|
||||
runTest {
|
||||
val json = Json { ignoreUnknownKeys = true }
|
||||
val pending = record(expiresAtMs = Long.MAX_VALUE)
|
||||
var getCalls = 0
|
||||
val controller =
|
||||
ChatController(
|
||||
scope = this,
|
||||
json = json,
|
||||
requestGateway = { method, _ ->
|
||||
when (method) {
|
||||
"question.list" -> json.encodeToString(QuestionListResult(emptyList()))
|
||||
"question.get" ->
|
||||
run {
|
||||
getCalls += 1
|
||||
throw GatewayRequestRejected(
|
||||
GatewaySession.ErrorShape(
|
||||
code = "INVALID_REQUEST",
|
||||
message = "question not found",
|
||||
details =
|
||||
GatewayConnectErrorDetails(
|
||||
code = null,
|
||||
reason = "QUESTION_NOT_FOUND",
|
||||
canRetryWithDeviceToken = false,
|
||||
recommendedNextStep = null,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
else -> "{}"
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
controller.handleGatewayEvent("question.requested", json.encodeToString(pending))
|
||||
advanceUntilIdle()
|
||||
|
||||
assertEquals(
|
||||
ChatQuestionStatus.Unavailable,
|
||||
controller.questions.value
|
||||
.single()
|
||||
.status(),
|
||||
)
|
||||
|
||||
controller.handleGatewayEvent("question.requested", json.encodeToString(pending))
|
||||
controller.handleGatewayEvent("health", null)
|
||||
advanceUntilIdle()
|
||||
|
||||
assertEquals(1, getCalls)
|
||||
assertEquals(
|
||||
ChatQuestionStatus.Unavailable,
|
||||
controller.questions.value
|
||||
.single()
|
||||
.status(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun skipUsesCancelResolutionAndKeepsSkippedSummary() =
|
||||
runTest {
|
||||
val json = Json { ignoreUnknownKeys = true }
|
||||
val pending = record(expiresAtMs = Long.MAX_VALUE)
|
||||
var resolveParams: String? = null
|
||||
val controller =
|
||||
ChatController(
|
||||
scope = this,
|
||||
json = json,
|
||||
requestGateway = { method, params ->
|
||||
when (method) {
|
||||
"question.list" -> json.encodeToString(QuestionListResult(listOf(pending)))
|
||||
"question.resolve" -> {
|
||||
resolveParams = params
|
||||
"{}"
|
||||
}
|
||||
else -> "{}"
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
controller.handleGatewayEvent("question.requested", json.encodeToString(pending))
|
||||
controller.skipQuestion(pending.id)
|
||||
advanceUntilIdle()
|
||||
|
||||
val params = json.parseToJsonElement(checkNotNull(resolveParams)).jsonObject
|
||||
assertEquals("ask_123", params["id"]?.jsonPrimitive?.content)
|
||||
assertTrue(params["cancel"]?.jsonPrimitive?.content?.toBoolean() == true)
|
||||
assertFalse("answers" in params)
|
||||
assertEquals(
|
||||
ChatQuestionStatus.Cancelled,
|
||||
controller.questions.value
|
||||
.single()
|
||||
.status(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun skipClaimExposesSkippingProgress() =
|
||||
runTest {
|
||||
val json = Json { ignoreUnknownKeys = true }
|
||||
val pending = record(expiresAtMs = Long.MAX_VALUE)
|
||||
val resolveStarted = CompletableDeferred<Unit>()
|
||||
val releaseResolve = CompletableDeferred<Unit>()
|
||||
val controller =
|
||||
ChatController(
|
||||
scope = this,
|
||||
json = json,
|
||||
requestGateway = { method, _ ->
|
||||
when (method) {
|
||||
"question.resolve" -> {
|
||||
resolveStarted.complete(Unit)
|
||||
releaseResolve.await()
|
||||
"{}"
|
||||
}
|
||||
else -> "{}"
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
controller.handleGatewayEvent("question.requested", json.encodeToString(pending))
|
||||
controller.skipQuestion(pending.id)
|
||||
runCurrent()
|
||||
resolveStarted.await()
|
||||
|
||||
val submitting = controller.questions.value.single()
|
||||
assertEquals(ChatQuestionStatus.Submitting, submitting.status())
|
||||
assertTrue(submitting.submitting)
|
||||
assertTrue(submitting.skipping)
|
||||
|
||||
releaseResolve.complete(Unit)
|
||||
advanceUntilIdle()
|
||||
|
||||
val completed = controller.questions.value.single()
|
||||
assertEquals(ChatQuestionStatus.Cancelled, completed.status())
|
||||
assertFalse(completed.submitting)
|
||||
assertFalse(completed.skipping)
|
||||
}
|
||||
|
||||
@Test
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun answerClaimBlocksCompetingSkip() =
|
||||
runTest {
|
||||
val json = Json { ignoreUnknownKeys = true }
|
||||
val pending = record(expiresAtMs = Long.MAX_VALUE)
|
||||
val requestStarted = CompletableDeferred<Unit>()
|
||||
val releaseRequest = CompletableDeferred<Unit>()
|
||||
val resolveParams = mutableListOf<String>()
|
||||
val controller =
|
||||
ChatController(
|
||||
scope = this,
|
||||
json = json,
|
||||
requestGateway = { method, params ->
|
||||
when (method) {
|
||||
"question.list" -> json.encodeToString(QuestionListResult(listOf(pending)))
|
||||
"question.resolve" -> {
|
||||
resolveParams.add(checkNotNull(params))
|
||||
requestStarted.complete(Unit)
|
||||
releaseRequest.await()
|
||||
"{}"
|
||||
}
|
||||
else -> "{}"
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
controller.handleGatewayEvent("question.requested", json.encodeToString(pending))
|
||||
controller.resolveQuestion(pending.id, mapOf("meal" to listOf("Pizza")))
|
||||
runCurrent()
|
||||
requestStarted.await()
|
||||
controller.skipQuestion(pending.id)
|
||||
releaseRequest.complete(Unit)
|
||||
advanceUntilIdle()
|
||||
|
||||
assertEquals(1, resolveParams.size)
|
||||
assertFalse("cancel" in resolveParams.single())
|
||||
assertEquals(
|
||||
ChatQuestionStatus.Answered,
|
||||
controller.questions.value
|
||||
.single()
|
||||
.status(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun successfulAnswerOverridesUnavailableRecoveryRace() =
|
||||
runTest {
|
||||
val json = Json { ignoreUnknownKeys = true }
|
||||
val pending = record(expiresAtMs = Long.MAX_VALUE)
|
||||
val resolveStarted = CompletableDeferred<Unit>()
|
||||
val releaseResolve = CompletableDeferred<Unit>()
|
||||
val controller =
|
||||
ChatController(
|
||||
scope = this,
|
||||
json = json,
|
||||
requestGateway = { method, _ ->
|
||||
when (method) {
|
||||
"question.list" ->
|
||||
json.encodeToString(
|
||||
QuestionListResult(if (resolveStarted.isCompleted) emptyList() else listOf(pending)),
|
||||
)
|
||||
"question.get" ->
|
||||
throw GatewayRequestRejected(
|
||||
GatewaySession.ErrorShape(
|
||||
code = "INVALID_REQUEST",
|
||||
message = "question not found",
|
||||
details =
|
||||
GatewayConnectErrorDetails(
|
||||
code = null,
|
||||
reason = "QUESTION_NOT_FOUND",
|
||||
canRetryWithDeviceToken = false,
|
||||
recommendedNextStep = null,
|
||||
),
|
||||
),
|
||||
)
|
||||
"question.resolve" -> {
|
||||
resolveStarted.complete(Unit)
|
||||
releaseResolve.await()
|
||||
"{}"
|
||||
}
|
||||
else -> "{}"
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
controller.handleGatewayEvent("question.requested", json.encodeToString(pending))
|
||||
runCurrent()
|
||||
controller.resolveQuestion(pending.id, mapOf("meal" to listOf("Pizza")))
|
||||
runCurrent()
|
||||
resolveStarted.await()
|
||||
controller.handleGatewayEvent("health", null)
|
||||
runCurrent()
|
||||
assertEquals(
|
||||
ChatQuestionStatus.Unavailable,
|
||||
controller.questions.value
|
||||
.single()
|
||||
.status(),
|
||||
)
|
||||
|
||||
releaseResolve.complete(Unit)
|
||||
advanceUntilIdle()
|
||||
|
||||
assertEquals(
|
||||
ChatQuestionStatus.Answered,
|
||||
controller.questions.value
|
||||
.single()
|
||||
.status(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun record(
|
||||
|
||||
+47
@@ -2,9 +2,14 @@ package ai.openclaw.app.ui.chat
|
||||
|
||||
import ai.openclaw.app.chat.ChatMessage
|
||||
import ai.openclaw.app.chat.ChatMessageContent
|
||||
import ai.openclaw.app.chat.ChatQuestionPrompt
|
||||
import ai.openclaw.app.gateway.QuestionAnswers
|
||||
import ai.openclaw.app.gateway.QuestionAnswersAnswersValue
|
||||
import ai.openclaw.app.gateway.QuestionRecord
|
||||
import androidx.compose.runtime.saveable.SaverScope
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
@@ -294,6 +299,39 @@ class ChatReaderScrollControllerTest {
|
||||
assertNull(transition.scrollIndex)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun questionTerminalStateAndHydratedAnswersChangeContentVersion() {
|
||||
val pending =
|
||||
ChatQuestionPrompt(
|
||||
QuestionRecord(
|
||||
id = "ask-1",
|
||||
questions = emptyList(),
|
||||
createdAtMs = 1_000,
|
||||
expiresAtMs = Long.MAX_VALUE,
|
||||
status = "pending",
|
||||
),
|
||||
)
|
||||
val pendingTimeline = questionTimeline(pending)
|
||||
val unavailableTimeline = questionTimeline(pending.copy(recoveryUnavailable = true))
|
||||
val answered = pending.copy(record = pending.record.copy(status = "answered"))
|
||||
val answeredWithoutValues = questionTimeline(answered)
|
||||
val answeredWithValues =
|
||||
questionTimeline(
|
||||
answered.copy(
|
||||
record =
|
||||
answered.record.copy(
|
||||
answers =
|
||||
QuestionAnswers(
|
||||
mapOf("choice" to QuestionAnswersAnswersValue(listOf("Yes"))),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
assertNotEquals(pendingTimeline.latestContentVersion, unavailableTimeline.latestContentVersion)
|
||||
assertNotEquals(answeredWithoutValues.latestContentVersion, answeredWithValues.latestContentVersion)
|
||||
}
|
||||
|
||||
private fun timeline(vararg messages: ChatMessage): ChatTimeline =
|
||||
buildChatTimeline(
|
||||
messages = messages.toList(),
|
||||
@@ -304,6 +342,15 @@ class ChatReaderScrollControllerTest {
|
||||
|
||||
private fun emptyTimeline(): ChatTimeline = timeline()
|
||||
|
||||
private fun questionTimeline(question: ChatQuestionPrompt): ChatTimeline =
|
||||
buildChatTimeline(
|
||||
messages = emptyList(),
|
||||
pendingRunCount = 0,
|
||||
pendingToolCalls = emptyList(),
|
||||
streamingAssistantText = null,
|
||||
questions = listOf(question),
|
||||
)
|
||||
|
||||
private fun activeTimeline(
|
||||
message: ChatMessage,
|
||||
stream: String?,
|
||||
|
||||
@@ -14689,6 +14689,278 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Answered": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Answered"
|
||||
}
|
||||
},
|
||||
"zh-CN": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "已回答"
|
||||
}
|
||||
},
|
||||
"zh-TW": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "已回答"
|
||||
}
|
||||
},
|
||||
"pt-BR": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Respondido"
|
||||
}
|
||||
},
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Beantwortet"
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Respondida"
|
||||
}
|
||||
},
|
||||
"ja-JP": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "回答済み"
|
||||
}
|
||||
},
|
||||
"ko": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "응답 완료"
|
||||
}
|
||||
},
|
||||
"fr": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Répondu"
|
||||
}
|
||||
},
|
||||
"hi": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "उत्तर दिया गया"
|
||||
}
|
||||
},
|
||||
"ar": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "تمت الإجابة"
|
||||
}
|
||||
},
|
||||
"it": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Risposto"
|
||||
}
|
||||
},
|
||||
"tr": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Yanıtlandı"
|
||||
}
|
||||
},
|
||||
"uk": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Відповідь надано"
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Sudah dijawab"
|
||||
}
|
||||
},
|
||||
"pl": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Udzielono odpowiedzi"
|
||||
}
|
||||
},
|
||||
"th": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "ตอบแล้ว"
|
||||
}
|
||||
},
|
||||
"vi": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Đã trả lời"
|
||||
}
|
||||
},
|
||||
"nl": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Beantwoord"
|
||||
}
|
||||
},
|
||||
"fa": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "پاسخ داده شده"
|
||||
}
|
||||
},
|
||||
"ru": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Отвечено"
|
||||
}
|
||||
},
|
||||
"sv": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Besvarad"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Answered elsewhere": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Answered elsewhere"
|
||||
}
|
||||
},
|
||||
"zh-CN": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "已在其他地方回答"
|
||||
}
|
||||
},
|
||||
"zh-TW": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "已在其他地方回答"
|
||||
}
|
||||
},
|
||||
"pt-BR": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Respondido em outro lugar"
|
||||
}
|
||||
},
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Anderswo beantwortet"
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Respondida en otro lugar"
|
||||
}
|
||||
},
|
||||
"ja-JP": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "他で回答済み"
|
||||
}
|
||||
},
|
||||
"ko": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "다른 곳에서 응답 완료"
|
||||
}
|
||||
},
|
||||
"fr": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Répondu ailleurs"
|
||||
}
|
||||
},
|
||||
"hi": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "कहीं और उत्तर दिया गया"
|
||||
}
|
||||
},
|
||||
"ar": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "تمت الإجابة في مكان آخر"
|
||||
}
|
||||
},
|
||||
"it": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Risposto altrove"
|
||||
}
|
||||
},
|
||||
"tr": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Başka yerde yanıtlandı"
|
||||
}
|
||||
},
|
||||
"uk": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Відповідь надано в іншому місці"
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Dijawab di tempat lain"
|
||||
}
|
||||
},
|
||||
"pl": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Odpowiedziano gdzie indziej"
|
||||
}
|
||||
},
|
||||
"th": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "ตอบจากที่อื่นแล้ว"
|
||||
}
|
||||
},
|
||||
"vi": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Đã trả lời ở nơi khác"
|
||||
}
|
||||
},
|
||||
"nl": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Elders beantwoord"
|
||||
}
|
||||
},
|
||||
"fa": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "در جای دیگری پاسخ داده شده"
|
||||
}
|
||||
},
|
||||
"ru": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Отвечено в другом месте"
|
||||
}
|
||||
},
|
||||
"sv": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Besvarad på annat håll"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Any binary: %@": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
@@ -64873,6 +65145,142 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Expired": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Expired"
|
||||
}
|
||||
},
|
||||
"zh-CN": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "已过期"
|
||||
}
|
||||
},
|
||||
"zh-TW": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "已過期"
|
||||
}
|
||||
},
|
||||
"pt-BR": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Expirado"
|
||||
}
|
||||
},
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Abgelaufen"
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Caducada"
|
||||
}
|
||||
},
|
||||
"ja-JP": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "期限切れ"
|
||||
}
|
||||
},
|
||||
"ko": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "만료됨"
|
||||
}
|
||||
},
|
||||
"fr": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Expiré"
|
||||
}
|
||||
},
|
||||
"hi": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "समय समाप्त"
|
||||
}
|
||||
},
|
||||
"ar": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "انتهت الصلاحية"
|
||||
}
|
||||
},
|
||||
"it": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Scaduto"
|
||||
}
|
||||
},
|
||||
"tr": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Süresi doldu"
|
||||
}
|
||||
},
|
||||
"uk": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Термін дії минув"
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Kedaluwarsa"
|
||||
}
|
||||
},
|
||||
"pl": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Wygasło"
|
||||
}
|
||||
},
|
||||
"th": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "หมดอายุแล้ว"
|
||||
}
|
||||
},
|
||||
"vi": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Đã hết hạn"
|
||||
}
|
||||
},
|
||||
"nl": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Verlopen"
|
||||
}
|
||||
},
|
||||
"fa": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "منقضی شده"
|
||||
}
|
||||
},
|
||||
"ru": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Срок истёк"
|
||||
}
|
||||
},
|
||||
"sv": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Utgången"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Expires": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
@@ -125801,6 +126209,142 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Question summary": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Question summary"
|
||||
}
|
||||
},
|
||||
"zh-CN": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "问题摘要"
|
||||
}
|
||||
},
|
||||
"zh-TW": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "問題摘要"
|
||||
}
|
||||
},
|
||||
"pt-BR": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Resumo da pergunta"
|
||||
}
|
||||
},
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Zusammenfassung der Frage"
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Resumen de la pregunta"
|
||||
}
|
||||
},
|
||||
"ja-JP": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "質問の概要"
|
||||
}
|
||||
},
|
||||
"ko": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "질문 요약"
|
||||
}
|
||||
},
|
||||
"fr": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Résumé de la question"
|
||||
}
|
||||
},
|
||||
"hi": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "प्रश्न का सारांश"
|
||||
}
|
||||
},
|
||||
"ar": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "ملخص السؤال"
|
||||
}
|
||||
},
|
||||
"it": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Riepilogo della domanda"
|
||||
}
|
||||
},
|
||||
"tr": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Soru özeti"
|
||||
}
|
||||
},
|
||||
"uk": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Підсумок запитання"
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Ringkasan pertanyaan"
|
||||
}
|
||||
},
|
||||
"pl": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Podsumowanie pytania"
|
||||
}
|
||||
},
|
||||
"th": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "สรุปคำถาม"
|
||||
}
|
||||
},
|
||||
"vi": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Tóm tắt câu hỏi"
|
||||
}
|
||||
},
|
||||
"nl": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Samenvatting van de vraag"
|
||||
}
|
||||
},
|
||||
"fa": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "خلاصه پرسش"
|
||||
}
|
||||
},
|
||||
"ru": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Краткое описание вопроса"
|
||||
}
|
||||
},
|
||||
"sv": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Frågesammanfattning"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Queue": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
@@ -159665,6 +160209,414 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Skip": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Skip"
|
||||
}
|
||||
},
|
||||
"zh-CN": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "跳过"
|
||||
}
|
||||
},
|
||||
"zh-TW": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "略過"
|
||||
}
|
||||
},
|
||||
"pt-BR": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Pular"
|
||||
}
|
||||
},
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Überspringen"
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Omitir"
|
||||
}
|
||||
},
|
||||
"ja-JP": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "スキップ"
|
||||
}
|
||||
},
|
||||
"ko": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "건너뛰기"
|
||||
}
|
||||
},
|
||||
"fr": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Ignorer"
|
||||
}
|
||||
},
|
||||
"hi": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "छोड़ें"
|
||||
}
|
||||
},
|
||||
"ar": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "تخطي"
|
||||
}
|
||||
},
|
||||
"it": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Salta"
|
||||
}
|
||||
},
|
||||
"tr": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Atla"
|
||||
}
|
||||
},
|
||||
"uk": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Пропустити"
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Lewati"
|
||||
}
|
||||
},
|
||||
"pl": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Pomiń"
|
||||
}
|
||||
},
|
||||
"th": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "ข้าม"
|
||||
}
|
||||
},
|
||||
"vi": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Bỏ qua"
|
||||
}
|
||||
},
|
||||
"nl": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Overslaan"
|
||||
}
|
||||
},
|
||||
"fa": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "رد کردن"
|
||||
}
|
||||
},
|
||||
"ru": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Пропустить"
|
||||
}
|
||||
},
|
||||
"sv": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Hoppa över"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Skipped": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Skipped"
|
||||
}
|
||||
},
|
||||
"zh-CN": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "已跳过"
|
||||
}
|
||||
},
|
||||
"zh-TW": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "已略過"
|
||||
}
|
||||
},
|
||||
"pt-BR": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Pulado"
|
||||
}
|
||||
},
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Übersprungen"
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Omitido"
|
||||
}
|
||||
},
|
||||
"ja-JP": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "スキップ済み"
|
||||
}
|
||||
},
|
||||
"ko": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "건너뜀"
|
||||
}
|
||||
},
|
||||
"fr": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Ignoré"
|
||||
}
|
||||
},
|
||||
"hi": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "छोड़ा गया"
|
||||
}
|
||||
},
|
||||
"ar": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "تم التخطي"
|
||||
}
|
||||
},
|
||||
"it": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Saltato"
|
||||
}
|
||||
},
|
||||
"tr": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Atlandı"
|
||||
}
|
||||
},
|
||||
"uk": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Пропущено"
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Dilewati"
|
||||
}
|
||||
},
|
||||
"pl": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Pominięto"
|
||||
}
|
||||
},
|
||||
"th": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "ข้ามแล้ว"
|
||||
}
|
||||
},
|
||||
"vi": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Đã bỏ qua"
|
||||
}
|
||||
},
|
||||
"nl": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Overgeslagen"
|
||||
}
|
||||
},
|
||||
"fa": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "رد شد"
|
||||
}
|
||||
},
|
||||
"ru": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Пропущено"
|
||||
}
|
||||
},
|
||||
"sv": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Överhoppad"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Skipping…": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Skipping…"
|
||||
}
|
||||
},
|
||||
"zh-CN": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "正在跳过…"
|
||||
}
|
||||
},
|
||||
"zh-TW": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "正在略過…"
|
||||
}
|
||||
},
|
||||
"pt-BR": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Pulando…"
|
||||
}
|
||||
},
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Wird übersprungen…"
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Omitiendo…"
|
||||
}
|
||||
},
|
||||
"ja-JP": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "スキップ中…"
|
||||
}
|
||||
},
|
||||
"ko": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "건너뛰는 중…"
|
||||
}
|
||||
},
|
||||
"fr": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Ignoré…"
|
||||
}
|
||||
},
|
||||
"hi": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "छोड़ा जा रहा है…"
|
||||
}
|
||||
},
|
||||
"ar": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "جارٍ التخطي…"
|
||||
}
|
||||
},
|
||||
"it": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Ignoro…"
|
||||
}
|
||||
},
|
||||
"tr": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Atlanıyor…"
|
||||
}
|
||||
},
|
||||
"uk": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Пропуск…"
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Melewati…"
|
||||
}
|
||||
},
|
||||
"pl": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Pomijanie…"
|
||||
}
|
||||
},
|
||||
"th": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "กำลังข้าม…"
|
||||
}
|
||||
},
|
||||
"vi": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Đang bỏ qua…"
|
||||
}
|
||||
},
|
||||
"nl": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Overslaan…"
|
||||
}
|
||||
},
|
||||
"fa": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "در حال رد کردن…"
|
||||
}
|
||||
},
|
||||
"ru": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Пропуск…"
|
||||
}
|
||||
},
|
||||
"sv": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Hoppar över…"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Source": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
|
||||
@@ -504,10 +504,19 @@ struct IOSGatewayChatTransport: OpenClawChatTransport {
|
||||
return try JSONDecoder().decode(QuestionListResult.self, from: data).questions
|
||||
}
|
||||
|
||||
func getQuestion(id: String) async throws -> QuestionRecord {
|
||||
let data = try await self.gateway.request(OpenClawChatGatewayRequests.questionGet(id: id))
|
||||
return try JSONDecoder().decode(QuestionGetResult.self, from: data).question
|
||||
}
|
||||
|
||||
func resolveQuestion(id: String, answers: [String: [String]]) async throws {
|
||||
_ = try await self.gateway.request(OpenClawChatGatewayRequests.resolveQuestion(id: id, answers: answers))
|
||||
}
|
||||
|
||||
func cancelQuestion(id: String) async throws {
|
||||
_ = try await self.gateway.request(OpenClawChatGatewayRequests.cancelQuestion(id: id))
|
||||
}
|
||||
|
||||
func events() -> AsyncStream<OpenClawChatTransportEvent> {
|
||||
AsyncStream { continuation in
|
||||
let task = Task {
|
||||
|
||||
@@ -568,11 +568,21 @@ struct MacGatewayChatTransport: OpenClawChatTransport {
|
||||
return try JSONDecoder().decode(QuestionListResult.self, from: data).questions
|
||||
}
|
||||
|
||||
func getQuestion(id: String) async throws -> QuestionRecord {
|
||||
let data = try await GatewayConnection.shared.request(OpenClawChatGatewayRequests.questionGet(id: id))
|
||||
return try JSONDecoder().decode(QuestionGetResult.self, from: data).question
|
||||
}
|
||||
|
||||
func resolveQuestion(id: String, answers: [String: [String]]) async throws {
|
||||
_ = try await GatewayConnection.shared.request(
|
||||
OpenClawChatGatewayRequests.resolveQuestion(id: id, answers: answers))
|
||||
}
|
||||
|
||||
func cancelQuestion(id: String) async throws {
|
||||
_ = try await GatewayConnection.shared.request(
|
||||
OpenClawChatGatewayRequests.cancelQuestion(id: id))
|
||||
}
|
||||
|
||||
func waitForRunCompletion(
|
||||
runId rawRunId: String,
|
||||
timeoutMs: Int) async -> OpenClawChatRunObservation
|
||||
|
||||
@@ -84,6 +84,13 @@ public enum OpenClawChatGatewayRequests {
|
||||
OpenClawChatGatewayRequest(method: "question.list", timeoutMs: self.defaultTimeoutMs)
|
||||
}
|
||||
|
||||
public static func questionGet(id: String) -> OpenClawChatGatewayRequest {
|
||||
OpenClawChatGatewayRequest(
|
||||
method: "question.get",
|
||||
params: ["id": AnyCodable(id)],
|
||||
timeoutMs: self.defaultTimeoutMs)
|
||||
}
|
||||
|
||||
public static func resolveQuestion(
|
||||
id: String,
|
||||
answers: [String: [String]]) -> OpenClawChatGatewayRequest
|
||||
@@ -98,6 +105,16 @@ public enum OpenClawChatGatewayRequests {
|
||||
timeoutMs: self.mutationTimeoutMs)
|
||||
}
|
||||
|
||||
public static func cancelQuestion(id: String) -> OpenClawChatGatewayRequest {
|
||||
OpenClawChatGatewayRequest(
|
||||
method: "question.resolve",
|
||||
params: [
|
||||
"id": AnyCodable(id),
|
||||
"cancel": AnyCodable(true),
|
||||
],
|
||||
timeoutMs: self.mutationTimeoutMs)
|
||||
}
|
||||
|
||||
public static func sessionsList(
|
||||
limit: Int?,
|
||||
search: String?,
|
||||
|
||||
@@ -11,38 +11,61 @@ public enum OpenClawQuestionCardStatus: Sendable, Equatable {
|
||||
case answeredElsewhere
|
||||
case expired
|
||||
case cancelled
|
||||
case unavailable
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
public final class OpenClawQuestionCardModel: Identifiable {
|
||||
private static let terminalRetentionSeconds: TimeInterval = 15
|
||||
|
||||
public let id: String
|
||||
public private(set) var record: QuestionRecord
|
||||
public private(set) var isSubmitting = false
|
||||
public private(set) var isSkipping = false
|
||||
public private(set) var wasAnsweredLocally = false
|
||||
public private(set) var errorText: String?
|
||||
public private(set) var selectedOptions: [String: Set<String>] = [:]
|
||||
public private(set) var otherText: [String: String] = [:]
|
||||
private var terminalObservedAt: Date?
|
||||
public private(set) var isLocallyExpired = false
|
||||
public private(set) var isRecoveryUnavailable = false
|
||||
|
||||
public init(record: QuestionRecord) {
|
||||
self.id = record.id
|
||||
self.record = record
|
||||
self.terminalObservedAt = record.status == .pending ? nil : Date()
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func apply(record: QuestionRecord, at date: Date = Date()) -> Bool {
|
||||
guard record.id == self.id, !Self.recordsMatch(self.record, record) else { return false }
|
||||
self.record = record
|
||||
self.isSubmitting = self.isSubmitting && record.status == .pending
|
||||
self.terminalObservedAt = record.status == .pending ? nil : (self.terminalObservedAt ?? date)
|
||||
public func apply(record: QuestionRecord) -> Bool {
|
||||
let nextRecord = self.preservingKnownAnswers(in: record)
|
||||
guard record.id == self.id,
|
||||
!((self.record.status != .pending || self.isRecoveryUnavailable) && record.status == .pending),
|
||||
!Self.recordsMatch(self.record, nextRecord)
|
||||
else { return false }
|
||||
self.record = nextRecord
|
||||
self.isSubmitting = self.isSubmitting && nextRecord.status == .pending
|
||||
self.isSkipping = self.isSkipping && nextRecord.status == .pending
|
||||
self.isLocallyExpired = false
|
||||
self.isRecoveryUnavailable = false
|
||||
return true
|
||||
}
|
||||
|
||||
private func preservingKnownAnswers(in record: QuestionRecord) -> QuestionRecord {
|
||||
guard record.status == .answered, record.answers == nil, let answers = self.record.answers else {
|
||||
return record
|
||||
}
|
||||
return QuestionRecord(
|
||||
id: record.id,
|
||||
questions: record.questions,
|
||||
agentid: record.agentid,
|
||||
sessionkey: record.sessionkey,
|
||||
createdatms: record.createdatms,
|
||||
expiresatms: record.expiresatms,
|
||||
status: record.status,
|
||||
answers: answers,
|
||||
resolvedby: record.resolvedby)
|
||||
}
|
||||
|
||||
public func status(at date: Date = Date()) -> OpenClawQuestionCardStatus {
|
||||
if self.isRecoveryUnavailable { return .unavailable }
|
||||
switch self.record.status {
|
||||
case .answered:
|
||||
return self.wasAnsweredLocally ? .answered : .answeredElsewhere
|
||||
@@ -51,7 +74,7 @@ public final class OpenClawQuestionCardModel: Identifiable {
|
||||
case .expired:
|
||||
return .expired
|
||||
case .pending:
|
||||
if date.timeIntervalSince1970 * 1000 >= Double(self.record.expiresatms) {
|
||||
if self.isLocallyExpired || date.timeIntervalSince1970 * 1000 >= Double(self.record.expiresatms) {
|
||||
return .expired
|
||||
}
|
||||
return self.isSubmitting ? .submitting : .pending
|
||||
@@ -84,6 +107,17 @@ public final class OpenClawQuestionCardModel: Identifiable {
|
||||
self.errorText = nil
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func toggleOption(questionID: String, optionNumber: Int) -> Bool {
|
||||
guard let question = self.record.questions.first(where: { $0.id == questionID }),
|
||||
self.status() == .pending,
|
||||
(1...4).contains(optionNumber),
|
||||
question.options.indices.contains(optionNumber - 1)
|
||||
else { return false }
|
||||
self.toggleOption(questionID: questionID, label: question.options[optionNumber - 1].label)
|
||||
return true
|
||||
}
|
||||
|
||||
public func setOtherText(questionID: String, value: String) {
|
||||
guard let question = self.record.questions.first(where: { $0.id == questionID }),
|
||||
question.options.isEmpty || question.isother == true,
|
||||
@@ -103,14 +137,25 @@ public final class OpenClawQuestionCardModel: Identifiable {
|
||||
public func beginSubmission() -> [String: [String]]? {
|
||||
guard let answers = self.answers(), self.status() == .pending else { return nil }
|
||||
self.isSubmitting = true
|
||||
self.isSkipping = false
|
||||
self.errorText = nil
|
||||
return answers
|
||||
}
|
||||
|
||||
public func markAnsweredLocally(at date: Date = Date()) {
|
||||
public func beginSkip() -> Bool {
|
||||
guard self.status() == .pending else { return false }
|
||||
self.isSubmitting = true
|
||||
self.isSkipping = true
|
||||
self.errorText = nil
|
||||
return true
|
||||
}
|
||||
|
||||
public func markAnsweredLocally(answers: [String: [String]]) {
|
||||
self.wasAnsweredLocally = true
|
||||
self.isSubmitting = false
|
||||
self.terminalObservedAt = self.terminalObservedAt ?? date
|
||||
self.isSkipping = false
|
||||
self.isLocallyExpired = false
|
||||
self.isRecoveryUnavailable = false
|
||||
self.record = QuestionRecord(
|
||||
id: self.record.id,
|
||||
questions: self.record.questions,
|
||||
@@ -119,18 +164,61 @@ public final class OpenClawQuestionCardModel: Identifiable {
|
||||
createdatms: self.record.createdatms,
|
||||
expiresatms: self.record.expiresatms,
|
||||
status: .answered,
|
||||
answers: self.answers().map { answers in
|
||||
QuestionAnswers(answers: answers.mapValues { values in
|
||||
AnyCodable(["answers": values])
|
||||
})
|
||||
},
|
||||
answers: QuestionAnswers(answers: answers.mapValues { values in
|
||||
AnyCodable(["answers": values])
|
||||
}),
|
||||
resolvedby: self.record.resolvedby)
|
||||
}
|
||||
|
||||
public func apply(resolved: OpenClawQuestionResolvedEvent, at date: Date = Date()) {
|
||||
public func markSkippedLocally() {
|
||||
self.isSubmitting = false
|
||||
self.isSkipping = false
|
||||
self.isLocallyExpired = false
|
||||
self.isRecoveryUnavailable = false
|
||||
self.record = QuestionRecord(
|
||||
id: self.record.id,
|
||||
questions: self.record.questions,
|
||||
agentid: self.record.agentid,
|
||||
sessionkey: self.record.sessionkey,
|
||||
createdatms: self.record.createdatms,
|
||||
expiresatms: self.record.expiresatms,
|
||||
status: .cancelled,
|
||||
resolvedby: self.record.resolvedby)
|
||||
}
|
||||
|
||||
// periphery:ignore - Public completion API for package consumers reconciling external answers.
|
||||
public func markAnsweredElsewhere() {
|
||||
self.isSubmitting = false
|
||||
self.isSkipping = false
|
||||
self.isLocallyExpired = false
|
||||
self.isRecoveryUnavailable = false
|
||||
self.record = QuestionRecord(
|
||||
id: self.record.id,
|
||||
questions: self.record.questions,
|
||||
agentid: self.record.agentid,
|
||||
sessionkey: self.record.sessionkey,
|
||||
createdatms: self.record.createdatms,
|
||||
expiresatms: self.record.expiresatms,
|
||||
status: .answered,
|
||||
resolvedby: self.record.resolvedby)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func markRecoveryUnavailable() -> Bool {
|
||||
guard !self.isRecoveryUnavailable else { return false }
|
||||
self.isSubmitting = false
|
||||
self.isSkipping = false
|
||||
self.isLocallyExpired = false
|
||||
self.isRecoveryUnavailable = true
|
||||
return true
|
||||
}
|
||||
|
||||
public func apply(resolved: OpenClawQuestionResolvedEvent) {
|
||||
guard resolved.id == self.id else { return }
|
||||
self.isSubmitting = false
|
||||
self.terminalObservedAt = self.terminalObservedAt ?? date
|
||||
self.isSkipping = false
|
||||
self.isLocallyExpired = false
|
||||
self.isRecoveryUnavailable = false
|
||||
self.record = QuestionRecord(
|
||||
id: self.record.id,
|
||||
questions: self.record.questions,
|
||||
@@ -139,38 +227,49 @@ public final class OpenClawQuestionCardModel: Identifiable {
|
||||
createdatms: self.record.createdatms,
|
||||
expiresatms: self.record.expiresatms,
|
||||
status: resolved.status,
|
||||
answers: resolved.answers,
|
||||
answers: resolved.answers ?? self.record.answers,
|
||||
resolvedby: self.record.resolvedby)
|
||||
}
|
||||
|
||||
public func failSubmission(_ message: String) {
|
||||
self.isSubmitting = false
|
||||
self.isSkipping = false
|
||||
self.errorText = message
|
||||
}
|
||||
|
||||
func shouldRetainAfterList(at date: Date) -> Bool {
|
||||
guard let terminalObservedAt else { return false }
|
||||
return date.timeIntervalSince(terminalObservedAt) < Self.terminalRetentionSeconds
|
||||
}
|
||||
|
||||
func terminalRetentionDelay(at date: Date) -> TimeInterval? {
|
||||
guard let terminalObservedAt else { return nil }
|
||||
return max(0, Self.terminalRetentionSeconds - date.timeIntervalSince(terminalObservedAt))
|
||||
}
|
||||
|
||||
func observeLocalExpiry(at date: Date) -> Bool {
|
||||
guard self.record.status == .pending, self.terminalObservedAt == nil,
|
||||
guard self.record.status == .pending, !self.isLocallyExpired,
|
||||
date.timeIntervalSince1970 * 1000 >= Double(self.record.expiresatms)
|
||||
else { return false }
|
||||
self.terminalObservedAt = date
|
||||
self.isLocallyExpired = true
|
||||
self.isSubmitting = false
|
||||
self.isSkipping = false
|
||||
return true
|
||||
}
|
||||
|
||||
func localExpiryDelay(at date: Date) -> TimeInterval? {
|
||||
guard self.record.status == .pending, self.terminalObservedAt == nil else { return nil }
|
||||
guard self.record.status == .pending, !self.isLocallyExpired else { return nil }
|
||||
return max(0, Double(self.record.expiresatms) / 1000 - date.timeIntervalSince1970)
|
||||
}
|
||||
|
||||
public func terminalSummaryText(for question: Question) -> String {
|
||||
switch self.status() {
|
||||
case .answered:
|
||||
self.answerValues(questionID: question.id)?.joined(separator: ", ") ?? String(localized: "Answered")
|
||||
case .answeredElsewhere:
|
||||
self.answerValues(questionID: question.id)?.joined(separator: ", ")
|
||||
?? String(localized: "Answered elsewhere")
|
||||
case .cancelled:
|
||||
String(localized: "Skipped")
|
||||
case .expired:
|
||||
String(localized: "Expired")
|
||||
case .unavailable:
|
||||
String(localized: "Unavailable")
|
||||
case .pending, .submitting:
|
||||
String(localized: "Pending")
|
||||
}
|
||||
}
|
||||
|
||||
private func answers() -> [String: [String]]? {
|
||||
var result: [String: [String]] = [:]
|
||||
for question in self.record.questions {
|
||||
@@ -187,6 +286,19 @@ public final class OpenClawQuestionCardModel: Identifiable {
|
||||
return result
|
||||
}
|
||||
|
||||
private func answerValues(questionID: String) -> [String]? {
|
||||
guard let answer = self.record.answers?.answers[questionID],
|
||||
let data = try? JSONEncoder().encode(answer),
|
||||
let decoded = try? JSONDecoder().decode(ResolvedAnswer.self, from: data),
|
||||
!decoded.answers.isEmpty
|
||||
else { return nil }
|
||||
return decoded.answers
|
||||
}
|
||||
|
||||
private struct ResolvedAnswer: Codable {
|
||||
let answers: [String]
|
||||
}
|
||||
|
||||
private static func recordsMatch(_ lhs: QuestionRecord, _ rhs: QuestionRecord) -> Bool {
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = [.sortedKeys]
|
||||
@@ -197,16 +309,31 @@ public final class OpenClawQuestionCardModel: Identifiable {
|
||||
struct OpenClawQuestionCard: View {
|
||||
@Bindable private var model: OpenClawQuestionCardModel
|
||||
private let onSubmit: @MainActor @Sendable (OpenClawQuestionCardModel) async -> Void
|
||||
private let onSkip: (@MainActor @Sendable (OpenClawQuestionCardModel) async -> Void)?
|
||||
#if os(macOS)
|
||||
@FocusState private var focusedQuestionID: String?
|
||||
#endif
|
||||
|
||||
init(
|
||||
model: OpenClawQuestionCardModel,
|
||||
onSubmit: @escaping @MainActor @Sendable (OpenClawQuestionCardModel) async -> Void)
|
||||
onSubmit: @escaping @MainActor @Sendable (OpenClawQuestionCardModel) async -> Void,
|
||||
onSkip: @escaping @MainActor @Sendable (OpenClawQuestionCardModel) async -> Void)
|
||||
{
|
||||
self.model = model
|
||||
self.onSubmit = onSubmit
|
||||
self.onSkip = onSkip
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
let status = self.model.status()
|
||||
if status == .pending || status == .submitting {
|
||||
self.pendingCard
|
||||
} else {
|
||||
self.terminalSummary
|
||||
}
|
||||
}
|
||||
|
||||
private var pendingCard: some View {
|
||||
TimelineView(.periodic(from: .now, by: 1)) { context in
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
ForEach(self.model.record.questions, id: \.id) { question in
|
||||
@@ -220,6 +347,26 @@ struct OpenClawQuestionCard: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var terminalSummary: some View {
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
ForEach(self.model.record.questions, id: \.id) { question in
|
||||
HStack(alignment: .firstTextBaseline, spacing: 5) {
|
||||
Text(verbatim: "\(question.header):")
|
||||
.font(OpenClawChatTypography.body(size: 14, weight: .semibold, relativeTo: .callout))
|
||||
Text(self.model.terminalSummaryText(for: question))
|
||||
.font(OpenClawChatTypography.body(size: 14, weight: .regular, relativeTo: .callout))
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 9)
|
||||
.background(OpenClawChatTheme.subtleCard, in: RoundedRectangle(cornerRadius: 12))
|
||||
.accessibilityElement(children: .contain)
|
||||
.accessibilityLabel("Question summary")
|
||||
}
|
||||
|
||||
private func questionSection(_ question: Question, now: Date) -> some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(question.header.uppercased())
|
||||
@@ -243,11 +390,30 @@ struct OpenClawQuestionCard: View {
|
||||
.accessibilityLabel("Other answer")
|
||||
}
|
||||
}
|
||||
#if os(macOS)
|
||||
.focusable()
|
||||
.focused(self.$focusedQuestionID, equals: question.id)
|
||||
.onKeyPress(characters: .decimalDigits) { keyPress in
|
||||
guard self.focusedQuestionID == question.id else { return .ignored }
|
||||
return self.handleNumberKey(keyPress, question: question, now: now)
|
||||
}
|
||||
.onKeyPress(.return) {
|
||||
guard self.focusedQuestionID == question.id,
|
||||
self.model.status(at: now) == .pending,
|
||||
self.model.canSubmit
|
||||
else { return .ignored }
|
||||
Task { await self.onSubmit(self.model) }
|
||||
return .handled
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private func optionRow(question: Question, option: QuestionOption, now: Date) -> some View {
|
||||
let selected = self.model.selectedOptions[question.id]?.contains(option.label) == true
|
||||
return Button {
|
||||
#if os(macOS)
|
||||
self.focusedQuestionID = question.id
|
||||
#endif
|
||||
self.model.toggleOption(questionID: question.id, label: option.label)
|
||||
} label: {
|
||||
HStack(alignment: .top, spacing: 10) {
|
||||
@@ -282,10 +448,32 @@ struct OpenClawQuestionCard: View {
|
||||
.font(OpenClawChatTypography.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
Spacer()
|
||||
Button(status == .submitting ? "Submitting…" : "Submit") {
|
||||
if let onSkip = self.onSkip {
|
||||
Button {
|
||||
Task { await onSkip(self.model) }
|
||||
} label: {
|
||||
if self.model.isSkipping {
|
||||
Text("Skipping…")
|
||||
.font(OpenClawChatTypography.body(size: 14, weight: .semibold, relativeTo: .callout))
|
||||
} else {
|
||||
Text("Skip")
|
||||
.font(OpenClawChatTypography.body(size: 14, weight: .semibold, relativeTo: .callout))
|
||||
}
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.disabled(status == .submitting)
|
||||
}
|
||||
Button {
|
||||
Task { await self.onSubmit(self.model) }
|
||||
} label: {
|
||||
if status == .submitting, !self.model.isSkipping {
|
||||
Text("Submitting…")
|
||||
.font(OpenClawChatTypography.body(size: 14, weight: .semibold, relativeTo: .callout))
|
||||
} else {
|
||||
Text("Submit")
|
||||
.font(OpenClawChatTypography.body(size: 14, weight: .semibold, relativeTo: .callout))
|
||||
}
|
||||
}
|
||||
.font(OpenClawChatTypography.body(size: 14, weight: .semibold, relativeTo: .callout))
|
||||
.buttonStyle(.borderedProminent)
|
||||
.disabled(!self.model.canSubmit || status == .submitting)
|
||||
}
|
||||
@@ -294,36 +482,27 @@ struct OpenClawQuestionCard: View {
|
||||
.font(OpenClawChatTypography.caption)
|
||||
.foregroundStyle(OpenClawChatTheme.danger)
|
||||
}
|
||||
} else {
|
||||
Label(self.terminalText(status), systemImage: self.terminalIcon(status))
|
||||
.font(OpenClawChatTypography.captionSemiBold)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
private func countdownText(now: Date) -> String {
|
||||
let seconds = self.model.remainingSeconds(at: now)
|
||||
return seconds >= 60 ? "Expires in \(seconds / 60)m \(seconds % 60)s" : "Expires in \(seconds)s"
|
||||
return String(format: "%d:%02d", seconds / 60, seconds % 60)
|
||||
}
|
||||
|
||||
private func terminalText(_ status: OpenClawQuestionCardStatus) -> String {
|
||||
switch status {
|
||||
case .answered: "Answered"
|
||||
case .answeredElsewhere: "Answered elsewhere"
|
||||
case .expired: "Expired"
|
||||
case .cancelled: "Cancelled"
|
||||
case .pending, .submitting: "Pending"
|
||||
}
|
||||
}
|
||||
|
||||
private func terminalIcon(_ status: OpenClawQuestionCardStatus) -> String {
|
||||
switch status {
|
||||
case .answered, .answeredElsewhere: "checkmark.circle.fill"
|
||||
case .expired: "clock.badge.xmark"
|
||||
case .cancelled: "xmark.circle"
|
||||
case .pending, .submitting: "clock"
|
||||
}
|
||||
#if os(macOS)
|
||||
private func handleNumberKey(
|
||||
_ keyPress: KeyPress,
|
||||
question: Question,
|
||||
now: Date) -> KeyPress.Result
|
||||
{
|
||||
guard self.model.status(at: now) == .pending,
|
||||
let digit = keyPress.characters.first?.wholeNumberValue,
|
||||
self.model.toggleOption(questionID: question.id, optionNumber: digit)
|
||||
else { return .ignored }
|
||||
return .handled
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@@ -334,12 +513,25 @@ struct OpenClawQuestionCards: View {
|
||||
ForEach(self.viewModel.visibleQuestionCards) { card in
|
||||
OpenClawQuestionCard(model: card) { [weak viewModel = self.viewModel] model in
|
||||
await viewModel?.submitQuestion(model)
|
||||
} onSkip: { [weak viewModel = self.viewModel] model in
|
||||
await viewModel?.skipQuestion(model)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum QuestionLookupResult {
|
||||
case record(QuestionRecord)
|
||||
case notFound
|
||||
case failed
|
||||
}
|
||||
|
||||
private struct QuestionRefreshApplyResult {
|
||||
let complete: Bool
|
||||
let changed: Bool
|
||||
}
|
||||
|
||||
extension OpenClawChatViewModel {
|
||||
public var visibleQuestionCards: [OpenClawQuestionCardModel] {
|
||||
self.questionCards.filter { card in
|
||||
@@ -354,44 +546,132 @@ extension OpenClawChatViewModel {
|
||||
func refreshQuestions() async {
|
||||
self.questionRefreshGeneration &+= 1
|
||||
let refreshGeneration = self.questionRefreshGeneration
|
||||
self.questionRefreshRetryTask?.cancel()
|
||||
self.questionRefreshRetryTask = nil
|
||||
await self.refreshQuestions(generation: refreshGeneration, retryIndex: 0)
|
||||
}
|
||||
|
||||
private func refreshQuestions(generation refreshGeneration: UInt64, retryIndex: Int) async {
|
||||
guard refreshGeneration == self.questionRefreshGeneration else { return }
|
||||
let stateRevision = self.questionStateRevision
|
||||
do {
|
||||
let records = try await self.transport.listQuestions()
|
||||
guard refreshGeneration == self.questionRefreshGeneration,
|
||||
stateRevision == self.questionStateRevision
|
||||
guard self.questionRefreshSnapshotIsCurrent(
|
||||
generation: refreshGeneration,
|
||||
stateRevision: stateRevision)
|
||||
else { return }
|
||||
let existing = Dictionary(uniqueKeysWithValues: self.questionCards.map { ($0.id, $0) })
|
||||
let listedIDs = Set(records.map(\.id))
|
||||
let now = Date()
|
||||
let retainedTerminal = self.questionCards.filter { model in
|
||||
!listedIDs.contains(model.id) && model.shouldRetainAfterList(at: now)
|
||||
let missingPending = self.questionCards.filter { model in
|
||||
model.record.status == .pending &&
|
||||
!model.isRecoveryUnavailable &&
|
||||
!listedIDs.contains(model.id)
|
||||
}
|
||||
var changed = records.count + retainedTerminal.count != self.questionCards.count
|
||||
self.questionCards = records.map { record in
|
||||
if let model = existing[record.id] {
|
||||
changed = model.apply(record: record) || changed
|
||||
return model
|
||||
}
|
||||
changed = true
|
||||
return OpenClawQuestionCardModel(record: record)
|
||||
} + retainedTerminal
|
||||
self.syncQuestionEvictions()
|
||||
if changed {
|
||||
self.questionStateRevision &+= 1
|
||||
self.markTimelineChanged()
|
||||
let lookups = await self.fetchMissingQuestionLookups(missingPending)
|
||||
guard self.questionRefreshSnapshotIsCurrent(
|
||||
generation: refreshGeneration,
|
||||
stateRevision: stateRevision)
|
||||
else { return }
|
||||
let result = self.applyQuestionRefresh(records: records, lookups: lookups)
|
||||
if result.complete {
|
||||
self.questionRefreshRetryTask = nil
|
||||
} else {
|
||||
self.scheduleQuestionRefreshRetry(
|
||||
generation: refreshGeneration,
|
||||
retryIndex: result.changed ? 0 : retryIndex)
|
||||
}
|
||||
} catch let error as GatewayResponseError where Self.questionListIsUnavailable(error) {
|
||||
guard refreshGeneration == self.questionRefreshGeneration,
|
||||
stateRevision == self.questionStateRevision,
|
||||
!self.questionCards.isEmpty
|
||||
guard self.questionRefreshSnapshotIsCurrent(
|
||||
generation: refreshGeneration,
|
||||
stateRevision: stateRevision)
|
||||
else { return }
|
||||
self.questionCards = []
|
||||
self.syncQuestionEvictions()
|
||||
self.clearPendingQuestionsForUnavailableList()
|
||||
} catch {
|
||||
guard self.questionRefreshSnapshotIsCurrent(
|
||||
generation: refreshGeneration,
|
||||
stateRevision: stateRevision)
|
||||
else { return }
|
||||
self.scheduleQuestionRefreshRetry(
|
||||
generation: refreshGeneration,
|
||||
retryIndex: retryIndex)
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchMissingQuestionLookups(
|
||||
_ models: [OpenClawQuestionCardModel]) async
|
||||
-> [(OpenClawQuestionCardModel, QuestionLookupResult)]
|
||||
{
|
||||
// The gateway enumerates pending questions only; terminal records remain addressable
|
||||
// briefly by known ID. Cards observed by this view model persist without timed eviction,
|
||||
// but session transcripts do not contain enough question data to recreate them on launch.
|
||||
var lookups: [(OpenClawQuestionCardModel, QuestionLookupResult)] = []
|
||||
for model in models {
|
||||
do {
|
||||
let record = try await self.transport.getQuestion(id: model.id)
|
||||
lookups.append((model, .record(record)))
|
||||
} catch let error as GatewayResponseError where Self.questionIsNotFound(error) {
|
||||
lookups.append((model, .notFound))
|
||||
} catch {
|
||||
lookups.append((model, .failed))
|
||||
}
|
||||
}
|
||||
return lookups
|
||||
}
|
||||
|
||||
private func applyQuestionRefresh(
|
||||
records: [QuestionRecord],
|
||||
lookups: [(OpenClawQuestionCardModel, QuestionLookupResult)]) -> QuestionRefreshApplyResult
|
||||
{
|
||||
var changed = false
|
||||
for record in records {
|
||||
if let model = self.questionCards.first(where: { $0.id == record.id }) {
|
||||
changed = model.apply(record: record) || changed
|
||||
} else {
|
||||
self.questionCards.append(OpenClawQuestionCardModel(record: record))
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
var complete = true
|
||||
for (model, result) in lookups {
|
||||
guard self.questionCards.contains(where: { $0 === model }) else { continue }
|
||||
switch result {
|
||||
case let .record(record):
|
||||
changed = model.apply(record: record) || changed
|
||||
case .notFound:
|
||||
// The terminal tombstone has aged out, so the question is no longer actionable,
|
||||
// but its answered/cancelled/expired outcome cannot be reconstructed.
|
||||
changed = model.markRecoveryUnavailable() || changed
|
||||
case .failed:
|
||||
complete = false
|
||||
}
|
||||
}
|
||||
self.syncQuestionExpirations()
|
||||
if changed {
|
||||
self.questionStateRevision &+= 1
|
||||
self.markTimelineChanged()
|
||||
} catch {
|
||||
// Question recovery is best-effort; chat bootstrap remains usable without this scope.
|
||||
}
|
||||
return QuestionRefreshApplyResult(complete: complete, changed: changed)
|
||||
}
|
||||
|
||||
private func clearPendingQuestionsForUnavailableList() {
|
||||
let previousCount = self.questionCards.count
|
||||
self.questionCards.removeAll {
|
||||
let status = $0.status()
|
||||
return status == .pending || status == .submitting
|
||||
}
|
||||
self.syncQuestionExpirations()
|
||||
if self.questionCards.count != previousCount {
|
||||
self.questionStateRevision &+= 1
|
||||
self.markTimelineChanged()
|
||||
}
|
||||
}
|
||||
|
||||
private func questionRefreshSnapshotIsCurrent(generation: UInt64, stateRevision: UInt64) -> Bool {
|
||||
guard generation == self.questionRefreshGeneration else { return false }
|
||||
guard stateRevision == self.questionStateRevision else {
|
||||
self.restartQuestionRefreshAfterStateChange(generation: generation)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private nonisolated static func questionListIsUnavailable(_ error: GatewayResponseError) -> Bool {
|
||||
@@ -400,6 +680,36 @@ extension OpenClawChatViewModel {
|
||||
error.message == "missing scope: operator.questions"
|
||||
}
|
||||
|
||||
private nonisolated static func questionIsNotFound(_ error: GatewayResponseError) -> Bool {
|
||||
error.detailsReason == "QUESTION_NOT_FOUND"
|
||||
}
|
||||
|
||||
private func restartQuestionRefreshAfterStateChange(generation: UInt64) {
|
||||
// Local question mutations invalidate the whole lookup snapshot, not one transport attempt.
|
||||
// Restart the bounded budget so a late mutation cannot consume the last reconciliation slot.
|
||||
self.scheduleQuestionRefreshRetry(generation: generation, retryIndex: 0)
|
||||
}
|
||||
|
||||
private func scheduleQuestionRefreshRetry(generation: UInt64, retryIndex: Int) {
|
||||
guard generation == self.questionRefreshGeneration else { return }
|
||||
guard self.questionRefreshRetryDelaysMs.indices.contains(retryIndex) else {
|
||||
self.questionRefreshRetryTask = nil
|
||||
return
|
||||
}
|
||||
let delayMs = self.questionRefreshRetryDelaysMs[retryIndex]
|
||||
let stateRevision = self.questionStateRevision
|
||||
self.questionRefreshRetryTask?.cancel()
|
||||
self.questionRefreshRetryTask = Task { [weak self] in
|
||||
try? await Task.sleep(for: .milliseconds(delayMs))
|
||||
guard !Task.isCancelled, let self,
|
||||
generation == self.questionRefreshGeneration
|
||||
else { return }
|
||||
// A mutation during backoff invalidates the reconciliation attempt, not the retry budget.
|
||||
let nextRetryIndex = stateRevision == self.questionStateRevision ? retryIndex + 1 : 0
|
||||
await self.refreshQuestions(generation: generation, retryIndex: nextRetryIndex)
|
||||
}
|
||||
}
|
||||
|
||||
func upsertQuestion(_ record: QuestionRecord) {
|
||||
if let model = self.questionCards.first(where: { $0.id == record.id }) {
|
||||
guard model.apply(record: record) else { return }
|
||||
@@ -407,14 +717,14 @@ extension OpenClawChatViewModel {
|
||||
self.questionCards.append(OpenClawQuestionCardModel(record: record))
|
||||
}
|
||||
self.questionStateRevision &+= 1
|
||||
self.syncQuestionEvictions()
|
||||
self.syncQuestionExpirations()
|
||||
self.markTimelineChanged()
|
||||
}
|
||||
|
||||
func resolveQuestionEvent(_ event: OpenClawQuestionResolvedEvent) {
|
||||
self.questionCards.first(where: { $0.id == event.id })?.apply(resolved: event)
|
||||
self.questionStateRevision &+= 1
|
||||
self.syncQuestionEvictions()
|
||||
self.syncQuestionExpirations()
|
||||
self.markTimelineChanged()
|
||||
}
|
||||
|
||||
@@ -422,6 +732,8 @@ extension OpenClawChatViewModel {
|
||||
// Invalidate a list snapshot captured before this event, then fetch the
|
||||
// authoritative set so other pending cards from that snapshot are not lost.
|
||||
self.questionRefreshGeneration &+= 1
|
||||
self.questionRefreshRetryTask?.cancel()
|
||||
self.questionRefreshRetryTask = nil
|
||||
Task { [weak self] in await self?.refreshQuestions() }
|
||||
}
|
||||
|
||||
@@ -430,9 +742,9 @@ extension OpenClawChatViewModel {
|
||||
self.questionStateRevision &+= 1
|
||||
do {
|
||||
try await self.transport.resolveQuestion(id: model.id, answers: answers)
|
||||
model.markAnsweredLocally()
|
||||
model.markAnsweredLocally(answers: answers)
|
||||
self.questionStateRevision &+= 1
|
||||
self.syncQuestionEvictions()
|
||||
self.syncQuestionExpirations()
|
||||
self.markTimelineChanged()
|
||||
} catch {
|
||||
model.failSubmission(error.localizedDescription)
|
||||
@@ -440,53 +752,63 @@ extension OpenClawChatViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
func evictQuestionIfTerminalGraceElapsed(
|
||||
func skipQuestion(_ model: OpenClawQuestionCardModel) async {
|
||||
guard model.beginSkip() else { return }
|
||||
self.questionStateRevision &+= 1
|
||||
do {
|
||||
try await self.transport.cancelQuestion(id: model.id)
|
||||
model.markSkippedLocally()
|
||||
self.questionStateRevision &+= 1
|
||||
self.syncQuestionExpirations()
|
||||
self.markTimelineChanged()
|
||||
} catch {
|
||||
model.failSubmission(error.localizedDescription)
|
||||
self.questionStateRevision &+= 1
|
||||
}
|
||||
}
|
||||
|
||||
func expireQuestionIfNeeded(
|
||||
_ model: OpenClawQuestionCardModel,
|
||||
at date: Date = Date())
|
||||
{
|
||||
guard self.questionCards.first(where: { $0.id == model.id }) === model else { return }
|
||||
if model.observeLocalExpiry(at: date) {
|
||||
self.questionStateRevision &+= 1
|
||||
self.syncQuestionExpirations(at: date)
|
||||
self.markTimelineChanged()
|
||||
Task { [weak self] in await self?.refreshQuestions() }
|
||||
} else {
|
||||
self.syncQuestionExpirations(at: date)
|
||||
}
|
||||
guard
|
||||
!model.shouldRetainAfterList(at: date),
|
||||
model.status(at: date) == .expired || model.record.status != .pending
|
||||
else {
|
||||
self.syncQuestionEvictions(at: date)
|
||||
return
|
||||
}
|
||||
self.questionCards.removeAll { $0 === model }
|
||||
self.questionEvictionTasks.removeValue(forKey: model.id)?.cancel()
|
||||
self.questionEvictionDeadlines.removeValue(forKey: model.id)
|
||||
self.questionStateRevision &+= 1
|
||||
self.markTimelineChanged()
|
||||
}
|
||||
|
||||
private func syncQuestionEvictions(at date: Date = Date()) {
|
||||
private func syncQuestionExpirations(at date: Date = Date()) {
|
||||
let modelsByID = Dictionary(uniqueKeysWithValues: self.questionCards.map { ($0.id, $0) })
|
||||
let cancelledIDs = self.questionEvictionTasks.keys.filter { modelsByID[$0] == nil }
|
||||
let cancelledIDs = self.questionExpiryTasks.keys.filter { modelsByID[$0] == nil }
|
||||
for id in cancelledIDs {
|
||||
self.questionEvictionTasks.removeValue(forKey: id)?.cancel()
|
||||
self.questionEvictionDeadlines.removeValue(forKey: id)
|
||||
self.questionExpiryTasks.removeValue(forKey: id)?.cancel()
|
||||
self.questionExpiryDeadlines.removeValue(forKey: id)
|
||||
}
|
||||
for model in self.questionCards {
|
||||
guard let delay = model.terminalRetentionDelay(at: date) ?? model.localExpiryDelay(at: date)
|
||||
else { continue }
|
||||
guard let delay = model.localExpiryDelay(at: date) else {
|
||||
self.questionExpiryTasks.removeValue(forKey: model.id)?.cancel()
|
||||
self.questionExpiryDeadlines.removeValue(forKey: model.id)
|
||||
continue
|
||||
}
|
||||
let deadline = date.addingTimeInterval(delay)
|
||||
if let scheduled = self.questionEvictionDeadlines[model.id],
|
||||
if let scheduled = self.questionExpiryDeadlines[model.id],
|
||||
abs(scheduled.timeIntervalSince(deadline)) < 0.01
|
||||
{
|
||||
continue
|
||||
}
|
||||
self.questionEvictionTasks.removeValue(forKey: model.id)?.cancel()
|
||||
self.questionEvictionDeadlines[model.id] = deadline
|
||||
self.questionEvictionTasks[model.id] = Task { [weak self, weak model] in
|
||||
self.questionExpiryTasks.removeValue(forKey: model.id)?.cancel()
|
||||
self.questionExpiryDeadlines[model.id] = deadline
|
||||
self.questionExpiryTasks[model.id] = Task { [weak self, weak model] in
|
||||
try? await Task.sleep(for: .seconds(delay))
|
||||
guard !Task.isCancelled, let self, let model else { return }
|
||||
self.questionEvictionTasks.removeValue(forKey: model.id)
|
||||
self.questionEvictionDeadlines.removeValue(forKey: model.id)
|
||||
self.evictQuestionIfTerminalGraceElapsed(model)
|
||||
self.questionExpiryTasks.removeValue(forKey: model.id)
|
||||
self.questionExpiryDeadlines.removeValue(forKey: model.id)
|
||||
self.expireQuestionIfNeeded(model)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -451,7 +451,9 @@ public protocol OpenClawChatTransport: Sendable {
|
||||
|
||||
func requestHealth(timeoutMs: Int) async throws -> Bool
|
||||
func listQuestions() async throws -> [QuestionRecord]
|
||||
func getQuestion(id: String) async throws -> QuestionRecord
|
||||
func resolveQuestion(id: String, answers: [String: [String]]) async throws
|
||||
func cancelQuestion(id: String) async throws
|
||||
func waitForRunCompletion(runId: String, timeoutMs: Int) async -> OpenClawChatRunObservation
|
||||
func events() -> AsyncStream<OpenClawChatTransportEvent>
|
||||
func resolveInlineWidgetResource(
|
||||
@@ -469,6 +471,13 @@ extension OpenClawChatTransport {
|
||||
[]
|
||||
}
|
||||
|
||||
public func getQuestion(id _: String) async throws -> QuestionRecord {
|
||||
throw NSError(
|
||||
domain: "OpenClawChatTransport",
|
||||
code: 0,
|
||||
userInfo: [NSLocalizedDescriptionKey: "question.get not supported by this transport"])
|
||||
}
|
||||
|
||||
public func resolveQuestion(id _: String, answers _: [String: [String]]) async throws {
|
||||
throw NSError(
|
||||
domain: "OpenClawChatTransport",
|
||||
@@ -476,6 +485,13 @@ extension OpenClawChatTransport {
|
||||
userInfo: [NSLocalizedDescriptionKey: "question.resolve not supported by this transport"])
|
||||
}
|
||||
|
||||
public func cancelQuestion(id _: String) async throws {
|
||||
throw NSError(
|
||||
domain: "OpenClawChatTransport",
|
||||
code: 0,
|
||||
userInfo: [NSLocalizedDescriptionKey: "question.resolve cancellation not supported by this transport"])
|
||||
}
|
||||
|
||||
public func requestFullMessage(sessionKey _: String, messageID _: String) async throws -> OpenClawChatMessage? {
|
||||
nil
|
||||
}
|
||||
|
||||
@@ -90,8 +90,10 @@ public final class OpenClawChatViewModel {
|
||||
public internal(set) var questionCards: [OpenClawQuestionCardModel] = []
|
||||
var questionRefreshGeneration: UInt64 = 0
|
||||
var questionStateRevision: UInt64 = 0
|
||||
var questionEvictionTasks: [String: Task<Void, Never>] = [:]
|
||||
var questionEvictionDeadlines: [String: Date] = [:]
|
||||
var questionExpiryTasks: [String: Task<Void, Never>] = [:]
|
||||
var questionExpiryDeadlines: [String: Date] = [:]
|
||||
var questionRefreshRetryTask: Task<Void, Never>?
|
||||
var questionRefreshRetryDelaysMs: [Int64] = [1000, 2000, 4000]
|
||||
var hasActiveSessionRunWithoutChatSnapshot = false
|
||||
|
||||
public private(set) var sessionKey: String {
|
||||
@@ -498,7 +500,8 @@ public final class OpenClawChatViewModel {
|
||||
self.outboxRetryTask?.cancel()
|
||||
self.outboxChangesTask?.cancel()
|
||||
self.activeSessionRunIndicatorTimeoutTask?.cancel()
|
||||
for (_, task) in self.questionEvictionTasks {
|
||||
self.questionRefreshRetryTask?.cancel()
|
||||
for (_, task) in self.questionExpiryTasks {
|
||||
task.cancel()
|
||||
}
|
||||
for (_, task) in self.pendingRunOwnerTasks {
|
||||
|
||||
@@ -228,6 +228,22 @@ struct ChatGatewayRequestTests {
|
||||
#expect(meal["answers"] as? [String] == ["Pizza", "Salad"])
|
||||
}
|
||||
|
||||
@Test func `question get request carries id`() {
|
||||
let request = OpenClawChatGatewayRequests.questionGet(id: "ask_123")
|
||||
|
||||
#expect(request.method == "question.get")
|
||||
#expect(request.params["id"]?.value as? String == "ask_123")
|
||||
}
|
||||
|
||||
@Test func `question cancel request uses resolve cancel contract`() {
|
||||
let request = OpenClawChatGatewayRequests.cancelQuestion(id: "ask_123")
|
||||
|
||||
#expect(request.method == "question.resolve")
|
||||
#expect(request.params["id"]?.value as? String == "ask_123")
|
||||
#expect(request.params["cancel"]?.value as? Bool == true)
|
||||
#expect(request.params["answers"] == nil)
|
||||
}
|
||||
|
||||
@Test func `long running requests share exact gateway timeout margins`() {
|
||||
#expect(OpenClawChatGatewayRequests.agentWait(runID: "run-1", timeoutMs: 1).timeoutMs == 5001)
|
||||
#expect(OpenClawChatGatewayRequests.agentWait(runID: "run-1", timeoutMs: 30000).timeoutMs == 35000)
|
||||
|
||||
@@ -9,7 +9,8 @@ private func questionRecord(
|
||||
isOther: Bool = true,
|
||||
createdAtMs: Int = 1_000_000,
|
||||
expiresAtMs: Int = 4_000_000_000_000,
|
||||
status: QuestionStatus = .pending) -> QuestionRecord
|
||||
status: QuestionStatus = .pending,
|
||||
answers: QuestionAnswers? = nil) -> QuestionRecord
|
||||
{
|
||||
QuestionRecord(
|
||||
id: "ask_123",
|
||||
@@ -29,7 +30,8 @@ private func questionRecord(
|
||||
sessionkey: "agent:main:main",
|
||||
createdatms: createdAtMs,
|
||||
expiresatms: expiresAtMs,
|
||||
status: status)
|
||||
status: status,
|
||||
answers: answers)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@@ -52,6 +54,16 @@ private func questionRecord(
|
||||
#expect(model.beginSubmission() == ["meal": ["Pizza", "Tacos"]])
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Test func `question card number selection uses declared option order`() {
|
||||
let model = OpenClawQuestionCardModel(record: questionRecord(multiSelect: true))
|
||||
|
||||
#expect(model.toggleOption(questionID: "meal", optionNumber: 2))
|
||||
#expect(model.toggleOption(questionID: "meal", optionNumber: 1))
|
||||
#expect(!model.toggleOption(questionID: "meal", optionNumber: 4))
|
||||
#expect(model.beginSubmission() == ["meal": ["Pizza", "Tacos"]])
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Test func `question card maps expiry and answer origin`() {
|
||||
let now = Date(timeIntervalSince1970: 1500)
|
||||
@@ -64,7 +76,7 @@ private func questionRecord(
|
||||
#expect(remote.status(at: Date(timeIntervalSince1970: 1500)) == .answeredElsewhere)
|
||||
|
||||
let local = OpenClawQuestionCardModel(record: questionRecord())
|
||||
local.markAnsweredLocally()
|
||||
local.markAnsweredLocally(answers: ["meal": ["Pizza"]])
|
||||
local.apply(resolved: OpenClawQuestionResolvedEvent(id: local.id, status: .answered))
|
||||
#expect(local.status(at: Date(timeIntervalSince1970: 1500)) == .answered)
|
||||
}
|
||||
@@ -83,32 +95,115 @@ private func questionRecord(
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Test func `question card retains terminal feedback for gateway grace`() {
|
||||
let observedAt = Date(timeIntervalSince1970: 1500)
|
||||
@Test func `question card ignores replayed pending record after terminal event`() {
|
||||
let model = OpenClawQuestionCardModel(record: questionRecord())
|
||||
model.apply(resolved: .init(id: model.id, status: .answered), at: observedAt)
|
||||
model.apply(resolved: .init(id: model.id, status: .answered))
|
||||
|
||||
#expect(model.shouldRetainAfterList(at: observedAt.addingTimeInterval(14)))
|
||||
#expect(!model.shouldRetainAfterList(at: observedAt.addingTimeInterval(15)))
|
||||
#expect(!model.apply(record: questionRecord(createdAtMs: 2_000_000)))
|
||||
#expect(model.status() == .answeredElsewhere)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Test func `question card locally expired state enters terminal retention`() {
|
||||
@Test func `question card preserves submitted answers across answerless refresh`() throws {
|
||||
let model = OpenClawQuestionCardModel(record: questionRecord())
|
||||
model.toggleOption(questionID: "meal", label: "Pizza")
|
||||
let answers = try #require(model.beginSubmission())
|
||||
model.markAnsweredLocally(answers: answers)
|
||||
|
||||
#expect(model.apply(record: questionRecord(createdAtMs: 2_000_000, status: .answered)))
|
||||
#expect(model.terminalSummaryText(for: model.record.questions[0]) == "Pizza")
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Test func `question card preserves submitted answers across answerless resolved event`() throws {
|
||||
let model = OpenClawQuestionCardModel(record: questionRecord())
|
||||
model.toggleOption(questionID: "meal", label: "Pizza")
|
||||
let answers = try #require(model.beginSubmission())
|
||||
model.markAnsweredLocally(answers: answers)
|
||||
|
||||
model.apply(resolved: .init(id: model.id, status: .answered))
|
||||
|
||||
#expect(model.terminalSummaryText(for: model.record.questions[0]) == "Pizza")
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Test func `question card locally expired state remains terminal`() {
|
||||
let expiresAt = Date(timeIntervalSince1970: 1500)
|
||||
let model = OpenClawQuestionCardModel(record: questionRecord(expiresAtMs: 1_500_000))
|
||||
|
||||
#expect(model.observeLocalExpiry(at: expiresAt))
|
||||
#expect(model.shouldRetainAfterList(at: expiresAt.addingTimeInterval(14)))
|
||||
#expect(!model.shouldRetainAfterList(at: expiresAt.addingTimeInterval(15)))
|
||||
#expect(!model.observeLocalExpiry(at: expiresAt.addingTimeInterval(15)))
|
||||
#expect(model.status(at: expiresAt.addingTimeInterval(15)) == .expired)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Test func `question card stores local answers in gateway record shape`() throws {
|
||||
let model = OpenClawQuestionCardModel(record: questionRecord())
|
||||
model.toggleOption(questionID: "meal", label: "Pizza")
|
||||
model.markAnsweredLocally()
|
||||
let answers = try #require(model.beginSubmission())
|
||||
model.markAnsweredLocally(answers: answers)
|
||||
|
||||
let data = try JSONEncoder().encode(model.record.answers)
|
||||
let json = try #require(String(data: data, encoding: .utf8))
|
||||
#expect(json.contains("\"meal\":{\"answers\":[\"Pizza\"]}"))
|
||||
#expect(model.terminalSummaryText(for: model.record.questions[0]) == "Pizza")
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Test func `question completions override unavailable recovery race`() throws {
|
||||
let answered = OpenClawQuestionCardModel(record: questionRecord())
|
||||
answered.toggleOption(questionID: "meal", label: "Pizza")
|
||||
let answers = try #require(answered.beginSubmission())
|
||||
answered.markRecoveryUnavailable()
|
||||
answered.markAnsweredLocally(answers: answers)
|
||||
#expect(answered.status() == .answered)
|
||||
|
||||
let skipped = OpenClawQuestionCardModel(record: questionRecord())
|
||||
#expect(skipped.beginSkip())
|
||||
skipped.markRecoveryUnavailable()
|
||||
skipped.markSkippedLocally()
|
||||
#expect(skipped.status() == .cancelled)
|
||||
|
||||
let answeredElsewhere = OpenClawQuestionCardModel(record: questionRecord())
|
||||
answeredElsewhere.markRecoveryUnavailable()
|
||||
answeredElsewhere.markAnsweredElsewhere()
|
||||
#expect(answeredElsewhere.status() == .answeredElsewhere)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Test func `question card terminal summaries prefer resolved answers`() {
|
||||
let answers = QuestionAnswers(answers: [
|
||||
"meal": AnyCodable(["answers": ["Pizza", "extra hot"]]),
|
||||
])
|
||||
let answered = OpenClawQuestionCardModel(record: questionRecord(status: .answered, answers: answers))
|
||||
let question = answered.record.questions[0]
|
||||
#expect(answered.terminalSummaryText(for: question) == "Pizza, extra hot")
|
||||
|
||||
let elsewhere = OpenClawQuestionCardModel(record: questionRecord(status: .answered))
|
||||
#expect(elsewhere.terminalSummaryText(for: question) == "Answered elsewhere")
|
||||
|
||||
let skipped = OpenClawQuestionCardModel(record: questionRecord(status: .cancelled))
|
||||
#expect(skipped.terminalSummaryText(for: question) == "Skipped")
|
||||
|
||||
let expired = OpenClawQuestionCardModel(record: questionRecord(status: .expired))
|
||||
#expect(expired.terminalSummaryText(for: question) == "Expired")
|
||||
|
||||
let unavailable = OpenClawQuestionCardModel(record: questionRecord())
|
||||
unavailable.markRecoveryUnavailable()
|
||||
#expect(unavailable.status() == .unavailable)
|
||||
#expect(unavailable.terminalSummaryText(for: question) == "Unavailable")
|
||||
#expect(!unavailable.apply(record: questionRecord()))
|
||||
#expect(unavailable.status() == .unavailable)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Test func `question card skip transitions to persistent skipped summary`() {
|
||||
let model = OpenClawQuestionCardModel(record: questionRecord())
|
||||
|
||||
#expect(model.beginSkip())
|
||||
#expect(model.isSkipping)
|
||||
model.markSkippedLocally()
|
||||
|
||||
#expect(model.status() == .cancelled)
|
||||
#expect(model.terminalSummaryText(for: model.record.questions[0]) == "Skipped")
|
||||
}
|
||||
|
||||
@@ -542,6 +542,18 @@ private actor AsyncCounter {
|
||||
}
|
||||
}
|
||||
|
||||
private actor AsyncStringRecorder {
|
||||
private var values: [String] = []
|
||||
|
||||
func append(_ value: String) {
|
||||
self.values.append(value)
|
||||
}
|
||||
|
||||
func current() -> [String] {
|
||||
self.values
|
||||
}
|
||||
}
|
||||
|
||||
private actor SessionSubscribeGate {
|
||||
private var waiters: [CheckedContinuation<Void, Never>] = []
|
||||
|
||||
@@ -640,6 +652,8 @@ private final class TestChatTransport: @unchecked Sendable, OpenClawChatTranspor
|
||||
(@Sendable (String, Int) async -> OpenClawChatRunObservation)?
|
||||
private let acquireSessionSettingsRouteLeaseHook: (@Sendable () async -> Void)?
|
||||
private let listQuestionsHook: (@Sendable () async throws -> [QuestionRecord])?
|
||||
private let getQuestionHook: (@Sendable (String) async throws -> QuestionRecord)?
|
||||
private let cancelQuestionHook: (@Sendable (String) async throws -> Void)?
|
||||
private let healthResponses: [Bool]
|
||||
|
||||
private let stream: AsyncStream<OpenClawChatTransportEvent>
|
||||
@@ -672,6 +686,8 @@ private final class TestChatTransport: @unchecked Sendable, OpenClawChatTranspor
|
||||
waitForRunCompletionHook: (@Sendable (String, Int) async -> OpenClawChatRunObservation)? = nil,
|
||||
acquireSessionSettingsRouteLeaseHook: (@Sendable () async -> Void)? = nil,
|
||||
listQuestionsHook: (@Sendable () async throws -> [QuestionRecord])? = nil,
|
||||
getQuestionHook: (@Sendable (String) async throws -> QuestionRecord)? = nil,
|
||||
cancelQuestionHook: (@Sendable (String) async throws -> Void)? = nil,
|
||||
healthResponses: [Bool] = [true])
|
||||
{
|
||||
self.historyResponses = historyResponses
|
||||
@@ -698,6 +714,8 @@ private final class TestChatTransport: @unchecked Sendable, OpenClawChatTranspor
|
||||
self.waitForRunCompletionHook = waitForRunCompletionHook
|
||||
self.acquireSessionSettingsRouteLeaseHook = acquireSessionSettingsRouteLeaseHook
|
||||
self.listQuestionsHook = listQuestionsHook
|
||||
self.getQuestionHook = getQuestionHook
|
||||
self.cancelQuestionHook = cancelQuestionHook
|
||||
self.healthResponses = healthResponses
|
||||
var cont: AsyncStream<OpenClawChatTransportEvent>.Continuation!
|
||||
self.stream = AsyncStream { c in
|
||||
@@ -989,6 +1007,20 @@ private final class TestChatTransport: @unchecked Sendable, OpenClawChatTranspor
|
||||
try await self.listQuestionsHook?() ?? []
|
||||
}
|
||||
|
||||
func getQuestion(id: String) async throws -> QuestionRecord {
|
||||
guard let getQuestionHook else {
|
||||
throw NSError(
|
||||
domain: "TestChatTransport",
|
||||
code: 0,
|
||||
userInfo: [NSLocalizedDescriptionKey: "missing question.get fixture"])
|
||||
}
|
||||
return try await getQuestionHook(id)
|
||||
}
|
||||
|
||||
func cancelQuestion(id: String) async throws {
|
||||
try await self.cancelQuestionHook?(id)
|
||||
}
|
||||
|
||||
func waitForRunCompletion(
|
||||
runId: String,
|
||||
timeoutMs: Int) async -> OpenClawChatRunObservation
|
||||
@@ -1285,7 +1317,9 @@ private actor QuestionListEventRace {
|
||||
private func chatQuestionRecord(
|
||||
id: String,
|
||||
status: QuestionStatus = .pending,
|
||||
expiresAtMs: Int = 4_000_000_000_000) -> QuestionRecord
|
||||
expiresAtMs: Int = 4_000_000_000_000,
|
||||
sessionKey: String? = "main",
|
||||
answers: QuestionAnswers? = nil) -> QuestionRecord
|
||||
{
|
||||
QuestionRecord(
|
||||
id: id,
|
||||
@@ -1297,15 +1331,16 @@ private func chatQuestionRecord(
|
||||
options: [QuestionOption(label: "One"), QuestionOption(label: "Two")]),
|
||||
],
|
||||
agentid: "main",
|
||||
sessionkey: "main",
|
||||
sessionkey: sessionKey,
|
||||
createdatms: 1,
|
||||
expiresatms: expiresAtMs,
|
||||
status: status)
|
||||
status: status,
|
||||
answers: answers)
|
||||
}
|
||||
|
||||
@Suite(.serialized)
|
||||
struct ChatViewModelTests {
|
||||
@Test @MainActor func `locally expired question is evicted after terminal grace`() {
|
||||
@Test @MainActor func `locally expired question remains in transcript`() {
|
||||
let viewModel = OpenClawChatViewModel(
|
||||
sessionKey: "main",
|
||||
transport: TestChatTransport(historyResponses: []))
|
||||
@@ -1313,13 +1348,14 @@ struct ChatViewModelTests {
|
||||
viewModel.upsertQuestion(chatQuestionRecord(id: "ask_local", expiresAtMs: 1_500_000))
|
||||
let model = viewModel.questionCards[0]
|
||||
|
||||
viewModel.evictQuestionIfTerminalGraceElapsed(model, at: expiresAt)
|
||||
viewModel.expireQuestionIfNeeded(model, at: expiresAt)
|
||||
#expect(viewModel.questionCards.map(\.id) == ["ask_local"])
|
||||
#expect(model.status(at: expiresAt) == .expired)
|
||||
|
||||
viewModel.evictQuestionIfTerminalGraceElapsed(
|
||||
viewModel.expireQuestionIfNeeded(
|
||||
model,
|
||||
at: expiresAt.addingTimeInterval(15))
|
||||
#expect(viewModel.questionCards.isEmpty)
|
||||
#expect(viewModel.questionCards.map(\.id) == ["ask_local"])
|
||||
}
|
||||
|
||||
@Test @MainActor func `stale question list cannot overwrite a newer event`() async throws {
|
||||
@@ -1408,7 +1444,7 @@ struct ChatViewModelTests {
|
||||
#expect(viewModel.questionCards.map(\.id) == ["ask_other"])
|
||||
}
|
||||
|
||||
@Test @MainActor func `question list retains recently resolved card`() async {
|
||||
@Test @MainActor func `question list retains resolved card persistently`() async {
|
||||
let transport = TestChatTransport(
|
||||
historyResponses: [],
|
||||
listQuestionsHook: { [] })
|
||||
@@ -1422,16 +1458,274 @@ struct ChatViewModelTests {
|
||||
#expect(viewModel.questionCards.first?.status() == .answeredElsewhere)
|
||||
}
|
||||
|
||||
@Test @MainActor func `question card is evicted after terminal grace`() {
|
||||
let transport = TestChatTransport(historyResponses: [])
|
||||
@Test @MainActor func `terminal question survives later empty list refresh`() async {
|
||||
let transport = TestChatTransport(historyResponses: [], listQuestionsHook: { [] })
|
||||
let viewModel = OpenClawChatViewModel(sessionKey: "main", transport: transport)
|
||||
viewModel.upsertQuestion(chatQuestionRecord(id: "ask_done"))
|
||||
viewModel.resolveQuestionEvent(.init(id: "ask_done", status: .answered))
|
||||
let model = viewModel.questionCards[0]
|
||||
|
||||
viewModel.evictQuestionIfTerminalGraceElapsed(model, at: Date().addingTimeInterval(16))
|
||||
await viewModel.refreshQuestions()
|
||||
|
||||
#expect(viewModel.questionCards.isEmpty)
|
||||
#expect(viewModel.questionCards.map(\.id) == ["ask_done"])
|
||||
#expect(viewModel.questionCards[0].status() == .answeredElsewhere)
|
||||
}
|
||||
|
||||
@Test @MainActor func `missing pending question uses question get fallback`() async {
|
||||
let answers = QuestionAnswers(answers: [
|
||||
"choice": AnyCodable(["answers": ["Two"]]),
|
||||
])
|
||||
let transport = TestChatTransport(
|
||||
historyResponses: [],
|
||||
listQuestionsHook: { [] },
|
||||
getQuestionHook: { id in
|
||||
chatQuestionRecord(id: id, status: .answered, answers: answers)
|
||||
})
|
||||
let viewModel = OpenClawChatViewModel(sessionKey: "main", transport: transport)
|
||||
viewModel.upsertQuestion(chatQuestionRecord(id: "ask_missing"))
|
||||
|
||||
await viewModel.refreshQuestions()
|
||||
|
||||
#expect(viewModel.questionCards[0].status() == .answeredElsewhere)
|
||||
#expect(viewModel.questionCards[0].terminalSummaryText(
|
||||
for: viewModel.questionCards[0].record.questions[0]) == "Two")
|
||||
}
|
||||
|
||||
@Test @MainActor func `missing question tombstone has unknown terminal outcome`() async {
|
||||
let getCalls = AsyncCounter()
|
||||
let transport = TestChatTransport(
|
||||
historyResponses: [],
|
||||
listQuestionsHook: { [] },
|
||||
getQuestionHook: { id in
|
||||
_ = await getCalls.increment()
|
||||
throw GatewayResponseError(
|
||||
method: "question.get",
|
||||
code: "INVALID_REQUEST",
|
||||
message: "question '\(id)' was not found",
|
||||
details: ["reason": AnyCodable("QUESTION_NOT_FOUND")])
|
||||
})
|
||||
let viewModel = OpenClawChatViewModel(sessionKey: "main", transport: transport)
|
||||
viewModel.upsertQuestion(chatQuestionRecord(id: "ask_missing"))
|
||||
|
||||
await viewModel.refreshQuestions()
|
||||
|
||||
#expect(viewModel.questionCards[0].status() == .unavailable)
|
||||
#expect(viewModel.questionCards[0].terminalSummaryText(
|
||||
for: viewModel.questionCards[0].record.questions[0]) == "Unavailable")
|
||||
|
||||
await viewModel.refreshQuestions()
|
||||
|
||||
#expect(await getCalls.current() == 1)
|
||||
}
|
||||
|
||||
@Test @MainActor func `question refresh retries transport failure`() async throws {
|
||||
let listCalls = AsyncCounter()
|
||||
let getCalls = AsyncCounter()
|
||||
let transport = TestChatTransport(
|
||||
historyResponses: [],
|
||||
listQuestionsHook: {
|
||||
let call = await listCalls.increment()
|
||||
if call == 1 {
|
||||
throw GatewayResponseError(
|
||||
method: "question.list",
|
||||
code: "UNAVAILABLE",
|
||||
message: "retry",
|
||||
details: nil)
|
||||
}
|
||||
return []
|
||||
},
|
||||
getQuestionHook: { id in
|
||||
_ = await getCalls.increment()
|
||||
return chatQuestionRecord(id: id, status: .cancelled)
|
||||
})
|
||||
let viewModel = OpenClawChatViewModel(sessionKey: "main", transport: transport)
|
||||
viewModel.questionRefreshRetryDelaysMs = [0]
|
||||
viewModel.upsertQuestion(chatQuestionRecord(id: "ask_retry"))
|
||||
|
||||
await viewModel.refreshQuestions()
|
||||
try await waitUntil("question refresh retry") { await getCalls.current() == 1 }
|
||||
|
||||
#expect(await listCalls.current() == 2)
|
||||
#expect(viewModel.questionCards[0].status() == .cancelled)
|
||||
}
|
||||
|
||||
@Test @MainActor func `question refresh resets exhausted retry budget after overlapping skip`() async throws {
|
||||
let listCalls = AsyncCounter()
|
||||
let getStarted = AsyncCounter()
|
||||
let getCalls = AsyncCounter()
|
||||
let firstGetGate = AsyncGate()
|
||||
let recovering = chatQuestionRecord(id: "ask_recovering")
|
||||
let unrelated = chatQuestionRecord(id: "ask_unrelated")
|
||||
let transport = TestChatTransport(
|
||||
historyResponses: [],
|
||||
listQuestionsHook: {
|
||||
let call = await listCalls.increment()
|
||||
if call == 1 {
|
||||
throw GatewayResponseError(
|
||||
method: "question.list",
|
||||
code: "UNAVAILABLE",
|
||||
message: "consume retry budget",
|
||||
details: nil)
|
||||
}
|
||||
return [unrelated]
|
||||
},
|
||||
getQuestionHook: { id in
|
||||
let call = await getCalls.increment()
|
||||
if call == 1 {
|
||||
_ = await getStarted.increment()
|
||||
await firstGetGate.wait()
|
||||
}
|
||||
return chatQuestionRecord(id: id, status: .answered)
|
||||
},
|
||||
cancelQuestionHook: { _ in })
|
||||
let viewModel = OpenClawChatViewModel(sessionKey: "main", transport: transport)
|
||||
viewModel.questionRefreshRetryDelaysMs = [0]
|
||||
viewModel.upsertQuestion(recovering)
|
||||
viewModel.upsertQuestion(unrelated)
|
||||
|
||||
let refresh = Task { await viewModel.refreshQuestions() }
|
||||
try await waitUntil("question get request") { await getStarted.current() == 1 }
|
||||
let unrelatedModel = try #require(viewModel.questionCards.first { $0.id == unrelated.id })
|
||||
await viewModel.skipQuestion(unrelatedModel)
|
||||
await firstGetGate.open()
|
||||
await refresh.value
|
||||
try await waitUntil("question reconciliation after skip") {
|
||||
await MainActor.run {
|
||||
viewModel.questionCards.first { $0.id == recovering.id }?.status() == .answeredElsewhere
|
||||
}
|
||||
}
|
||||
|
||||
#expect(await getCalls.current() == 2)
|
||||
#expect(await listCalls.current() == 3)
|
||||
}
|
||||
|
||||
@Test @MainActor func `question refresh resets exhausted retry budget after partial progress`() async throws {
|
||||
let listCalls = AsyncCounter()
|
||||
let recoveringCalls = AsyncCounter()
|
||||
let progressed = chatQuestionRecord(id: "ask_progressed")
|
||||
let recovering = chatQuestionRecord(id: "ask_recovering")
|
||||
let transport = TestChatTransport(
|
||||
historyResponses: [],
|
||||
listQuestionsHook: {
|
||||
let call = await listCalls.increment()
|
||||
if call == 1 {
|
||||
throw GatewayResponseError(
|
||||
method: "question.list",
|
||||
code: "UNAVAILABLE",
|
||||
message: "consume retry budget",
|
||||
details: nil)
|
||||
}
|
||||
return []
|
||||
},
|
||||
getQuestionHook: { id in
|
||||
if id == progressed.id {
|
||||
return chatQuestionRecord(id: id, status: .answered)
|
||||
}
|
||||
let call = await recoveringCalls.increment()
|
||||
if call == 1 {
|
||||
throw GatewayResponseError(
|
||||
method: "question.get",
|
||||
code: "UNAVAILABLE",
|
||||
message: "partial failure",
|
||||
details: nil)
|
||||
}
|
||||
return chatQuestionRecord(id: id, status: .cancelled)
|
||||
})
|
||||
let viewModel = OpenClawChatViewModel(sessionKey: "main", transport: transport)
|
||||
viewModel.questionRefreshRetryDelaysMs = [0]
|
||||
viewModel.upsertQuestion(progressed)
|
||||
viewModel.upsertQuestion(recovering)
|
||||
|
||||
await viewModel.refreshQuestions()
|
||||
try await waitUntil("question retry after partial progress") {
|
||||
await MainActor.run {
|
||||
viewModel.questionCards.first { $0.id == recovering.id }?.status() == .cancelled
|
||||
}
|
||||
}
|
||||
|
||||
#expect(await listCalls.current() == 3)
|
||||
#expect(await recoveringCalls.current() == 2)
|
||||
#expect(viewModel.questionCards.first { $0.id == progressed.id }?.status() == .answeredElsewhere)
|
||||
}
|
||||
|
||||
@Test @MainActor func `question refresh resets retry budget after state change during backoff`() async throws {
|
||||
let listCalls = AsyncCounter()
|
||||
let transport = TestChatTransport(
|
||||
historyResponses: [],
|
||||
listQuestionsHook: {
|
||||
let call = await listCalls.increment()
|
||||
if call < 3 {
|
||||
throw GatewayResponseError(
|
||||
method: "question.list",
|
||||
code: "UNAVAILABLE",
|
||||
message: "retry",
|
||||
details: nil)
|
||||
}
|
||||
return []
|
||||
})
|
||||
let viewModel = OpenClawChatViewModel(sessionKey: "main", transport: transport)
|
||||
viewModel.questionRefreshRetryDelaysMs = [25]
|
||||
let question = chatQuestionRecord(id: "ask_backoff")
|
||||
viewModel.upsertQuestion(question)
|
||||
|
||||
await viewModel.refreshQuestions()
|
||||
viewModel.resolveQuestionEvent(.init(id: question.id, status: .cancelled))
|
||||
try await waitUntil("question retry budget reset after backoff mutation") {
|
||||
await listCalls.current() == 3
|
||||
}
|
||||
|
||||
#expect(viewModel.questionCards[0].status() == .cancelled)
|
||||
}
|
||||
|
||||
@Test @MainActor func `question refresh stops after bounded retries`() async throws {
|
||||
let listCalls = AsyncCounter()
|
||||
let transport = TestChatTransport(
|
||||
historyResponses: [],
|
||||
listQuestionsHook: {
|
||||
_ = await listCalls.increment()
|
||||
throw GatewayResponseError(
|
||||
method: "question.list",
|
||||
code: "UNAVAILABLE",
|
||||
message: "retry",
|
||||
details: nil)
|
||||
})
|
||||
let viewModel = OpenClawChatViewModel(sessionKey: "main", transport: transport)
|
||||
viewModel.questionRefreshRetryDelaysMs = [0, 0, 0]
|
||||
|
||||
await viewModel.refreshQuestions()
|
||||
try await waitUntil("bounded question refresh retries") { await listCalls.current() >= 4 }
|
||||
try await Task.sleep(for: .milliseconds(25))
|
||||
|
||||
#expect(await listCalls.current() == 4)
|
||||
#expect(viewModel.questionRefreshRetryTask == nil)
|
||||
}
|
||||
|
||||
@Test @MainActor func `visible questions filter by current session`() {
|
||||
let viewModel = OpenClawChatViewModel(
|
||||
sessionKey: "main",
|
||||
transport: TestChatTransport(historyResponses: []))
|
||||
viewModel.upsertQuestion(chatQuestionRecord(id: "ask_main"))
|
||||
viewModel.upsertQuestion(chatQuestionRecord(id: "ask_other", sessionKey: "other"))
|
||||
viewModel.upsertQuestion(chatQuestionRecord(id: "ask_unscoped", sessionKey: nil))
|
||||
|
||||
#expect(viewModel.visibleQuestionCards.map(\.id) == ["ask_main", "ask_unscoped"])
|
||||
}
|
||||
|
||||
@Test @MainActor func `skip sends question cancellation and retains summary`() async {
|
||||
let cancelledIDs = AsyncStringRecorder()
|
||||
let transport = TestChatTransport(
|
||||
historyResponses: [],
|
||||
cancelQuestionHook: { id in
|
||||
await cancelledIDs.append(id)
|
||||
})
|
||||
let viewModel = OpenClawChatViewModel(sessionKey: "main", transport: transport)
|
||||
viewModel.upsertQuestion(chatQuestionRecord(id: "ask_skip"))
|
||||
|
||||
await viewModel.skipQuestion(viewModel.questionCards[0])
|
||||
|
||||
#expect(await cancelledIDs.current() == ["ask_skip"])
|
||||
#expect(viewModel.questionCards[0].status() == .cancelled)
|
||||
#expect(viewModel.questionCards[0].terminalSummaryText(
|
||||
for: viewModel.questionCards[0].record.questions[0]) == "Skipped")
|
||||
}
|
||||
|
||||
@Test func `context usage fraction validates freshness and token bounds`() {
|
||||
|
||||
@@ -9530,6 +9530,7 @@ Do not edit it by hand; run `pnpm docs:map:gen`.
|
||||
- Route: /tools/ask-user
|
||||
- Headings:
|
||||
- H2: Answer a question
|
||||
- H2: Platform behavior
|
||||
- H2: Timeout and no answer
|
||||
- H2: Tool schema
|
||||
- H2: Model guidance
|
||||
|
||||
@@ -31,6 +31,15 @@ You can answer from any supported conversation surface:
|
||||
OpenClaw always enables a free-text **Other** answer. The agent must not add an
|
||||
`Other` option to the authored option list.
|
||||
|
||||
## Platform behavior
|
||||
|
||||
Answers work on every supported conversation surface. The web Control UI uses a
|
||||
docked stepper that replaces the composer while expanded; collapsing it restores
|
||||
the full composer beneath a slim question bar. iOS, macOS, and Android show
|
||||
inline cards; multiple questions stay stacked as an intentional touch-friendly
|
||||
idiom. Every platform keeps the question-to-answer summary in the active chat
|
||||
timeline without timed eviction, and **Skip** is available everywhere.
|
||||
|
||||
Prompts that cannot use native buttons, including multi-question and
|
||||
multi-select prompts, degrade to readable text on channels. The Control UI
|
||||
keeps the full structured stepper.
|
||||
|
||||
@@ -52,6 +52,7 @@ const schemaNames = new Map<string, string>([
|
||||
["Question", "Question"],
|
||||
["QuestionAnswers", "QuestionAnswers"],
|
||||
["QuestionRecord", "QuestionRecord"],
|
||||
["QuestionGetResult", "QuestionGetResult"],
|
||||
["QuestionListResult", "QuestionListResult"],
|
||||
]);
|
||||
|
||||
|
||||
@@ -537,7 +537,7 @@ describe("refreshPendingQuestions", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("terminalizes a missing pending question after authoritative not-found", async () => {
|
||||
it("terminalizes a missing pending question with an unknown outcome after not-found", async () => {
|
||||
const request = vi.fn<RequestFn>(async (method) => {
|
||||
if (method === "question.list") {
|
||||
return { questions: [] };
|
||||
@@ -553,11 +553,11 @@ describe("refreshPendingQuestions", () => {
|
||||
});
|
||||
|
||||
refreshPendingQuestionsWithRetry(state, client);
|
||||
await waitForFast(() => expect(state.prompts.get("question-1")?.status).toBe("answered"));
|
||||
await waitForFast(() => expect(state.prompts.get("question-1")?.status).toBe("unavailable"));
|
||||
|
||||
expect(state.prompts.get("question-1")).toMatchObject({
|
||||
status: "answered",
|
||||
answeredElsewhere: true,
|
||||
status: "unavailable",
|
||||
answeredElsewhere: false,
|
||||
locallyExpired: false,
|
||||
submitting: false,
|
||||
error: null,
|
||||
|
||||
@@ -16,6 +16,8 @@ type QuestionDraft = {
|
||||
freeText: string;
|
||||
};
|
||||
|
||||
type QuestionPromptStatus = QuestionRecord["status"] | "unavailable";
|
||||
|
||||
export type QuestionPrompt = {
|
||||
id: string;
|
||||
questions: Question[];
|
||||
@@ -23,7 +25,7 @@ export type QuestionPrompt = {
|
||||
sessionKey?: string;
|
||||
createdAtMs: number;
|
||||
expiresAtMs: number;
|
||||
status: QuestionRecord["status"];
|
||||
status: QuestionPromptStatus;
|
||||
answers?: QuestionAnswers;
|
||||
submittedAnswers?: QuestionAnswers;
|
||||
answeredElsewhere: boolean;
|
||||
@@ -388,10 +390,12 @@ function isQuestionNotFoundError(error: unknown): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function markResolvedElsewhere(state: QuestionPromptState, prompt: QuestionPrompt): void {
|
||||
prompt.status = "answered";
|
||||
function markRecoveryUnavailable(state: QuestionPromptState, prompt: QuestionPrompt): void {
|
||||
// QUESTION_NOT_FOUND means the gateway tombstone aged out. It proves the prompt is
|
||||
// no longer actionable, but not whether it was answered, cancelled, or expired.
|
||||
prompt.status = "unavailable";
|
||||
prompt.answers = undefined;
|
||||
prompt.answeredElsewhere = true;
|
||||
prompt.answeredElsewhere = false;
|
||||
prompt.localResolutionConfirmed = false;
|
||||
prompt.locallyExpired = false;
|
||||
prompt.submitting = false;
|
||||
@@ -474,7 +478,7 @@ async function refreshPendingQuestions(
|
||||
missingResult?.status === "rejected" &&
|
||||
isQuestionNotFoundError(missingResult.reason)
|
||||
) {
|
||||
markResolvedElsewhere(state, current);
|
||||
markRecoveryUnavailable(state, current);
|
||||
continue;
|
||||
}
|
||||
const record =
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Control UI E2E tests cover docked Gateway questions through the mocked WebSocket.
|
||||
// Control UI E2E tests cover composer-replacing Gateway questions through the mocked WebSocket.
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { chromium, type Browser, type BrowserContext, type Page } from "playwright";
|
||||
@@ -67,7 +67,7 @@ function historyMessages() {
|
||||
text:
|
||||
index === 11
|
||||
? "I have the release context ready. I only need your deployment choice."
|
||||
: `Release preparation note ${index + 1}: deterministic transcript content for the docked panel proof.`,
|
||||
: `Release preparation note ${index + 1}: deterministic transcript content for the question panel proof.`,
|
||||
},
|
||||
],
|
||||
timestamp: 1_750_000_000_000 + index * 1_000,
|
||||
@@ -123,8 +123,10 @@ describeControlUiE2e("Control UI Gateway question flow", () => {
|
||||
await server?.close();
|
||||
});
|
||||
|
||||
it("docks a pending question and replaces it with a compact answer summary", async () => {
|
||||
it("replaces the composer, restores it on collapse, and preserves its draft through resolution", async () => {
|
||||
const { gateway, page } = await openQuestionPage();
|
||||
const composer = page.locator(".agent-chat__composer-combobox textarea");
|
||||
await composer.fill("Keep this release note draft");
|
||||
const request = questionRecord("question-deploy-target", [
|
||||
{
|
||||
id: "deploy_target",
|
||||
@@ -152,17 +154,29 @@ describeControlUiE2e("Control UI Gateway question flow", () => {
|
||||
.toBe(0);
|
||||
await expect.poll(() => panel.getByText("1/1", { exact: true }).count()).toBe(1);
|
||||
await expect.poll(() => panel.getByPlaceholder("Type your own answer here").count()).toBe(1);
|
||||
await expect.poll(() => page.locator(".agent-chat__input").count()).toBe(0);
|
||||
await expect.poll(() => page.locator(".agent-chat__composer-footer").count()).toBe(0);
|
||||
await expect
|
||||
.poll(() =>
|
||||
panel
|
||||
.locator(".chat-question-panel")
|
||||
.evaluate((element) => document.activeElement === element),
|
||||
)
|
||||
.toBe(true);
|
||||
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const panelBox = await panel.boundingBox();
|
||||
const composerBox = await page.locator(".agent-chat__input").boundingBox();
|
||||
if (!panelBox || !composerBox) {
|
||||
const shellBox = await page.locator(".agent-chat__composer-shell").boundingBox();
|
||||
if (!panelBox || !shellBox) {
|
||||
return null;
|
||||
}
|
||||
return Math.round(composerBox.y - (panelBox.y + panelBox.height));
|
||||
return {
|
||||
left: Math.round(panelBox.x - shellBox.x),
|
||||
width: Math.round(panelBox.width - shellBox.width),
|
||||
};
|
||||
})
|
||||
.toBeGreaterThanOrEqual(0);
|
||||
.toEqual({ left: 0, width: 0 });
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const panelHeight = (await panel.boundingBox())?.height ?? 0;
|
||||
@@ -174,6 +188,22 @@ describeControlUiE2e("Control UI Gateway question flow", () => {
|
||||
.toBe(true);
|
||||
await screenshot(page, "01-question-pending.png");
|
||||
|
||||
await panel.locator(".chat-question-panel__collapse").click();
|
||||
await composer.waitFor();
|
||||
await expect.poll(() => composer.inputValue()).toBe("Keep this release note draft");
|
||||
await expect
|
||||
.poll(() => composer.evaluate((element) => document.activeElement === element))
|
||||
.toBe(true);
|
||||
await page.locator(".chat-question-panel__collapsed-button").click();
|
||||
await expect.poll(() => page.locator(".agent-chat__input").count()).toBe(0);
|
||||
await expect
|
||||
.poll(() =>
|
||||
panel
|
||||
.locator(".chat-question-panel")
|
||||
.evaluate((element) => document.activeElement === element),
|
||||
)
|
||||
.toBe(true);
|
||||
|
||||
await panel.getByRole("radio", { name: /Staging \(Recommended\)/ }).click();
|
||||
await panel.getByRole("button", { name: "Submit", exact: true }).click();
|
||||
const resolveRequest = await gateway.waitForRequest("question.resolve");
|
||||
@@ -201,6 +231,11 @@ describeControlUiE2e("Control UI Gateway question flow", () => {
|
||||
await expect
|
||||
.poll(() => summary.getByText("Staging (Recommended)", { exact: true }).count())
|
||||
.toBe(1);
|
||||
await composer.waitFor();
|
||||
await expect.poll(() => composer.inputValue()).toBe("Keep this release note draft");
|
||||
await expect
|
||||
.poll(() => composer.evaluate((element) => document.activeElement === element))
|
||||
.toBe(true);
|
||||
await screenshot(page, "02-question-answered.png");
|
||||
});
|
||||
|
||||
@@ -304,9 +339,9 @@ describeControlUiE2e("Control UI Gateway question flow", () => {
|
||||
page.locator(".chat-question-summary").filter({ hasText: "Release manager" }).count(),
|
||||
)
|
||||
.toBe(1);
|
||||
await expect
|
||||
.poll(() => page.locator(".chat-question-summary").filter({ hasText: "Expired" }).count())
|
||||
.toBe(1);
|
||||
const expiredSummary = page.locator(".chat-question-summary").filter({ hasText: "Expired" });
|
||||
await expect.poll(() => expiredSummary.count()).toBe(1);
|
||||
await expiredSummary.scrollIntoViewIfNeeded();
|
||||
await screenshot(page, "04-question-terminal-states.png");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3772,6 +3772,7 @@ export const en: TranslationMap = {
|
||||
submitFailed: "Could not submit: {error}",
|
||||
answered: "Answered",
|
||||
answeredElsewhere: "Answered elsewhere",
|
||||
unavailable: "Unavailable",
|
||||
expired: "Expired",
|
||||
cancelled: "Cancelled",
|
||||
},
|
||||
|
||||
@@ -313,6 +313,71 @@ describe("renderChatComposer controls", () => {
|
||||
});
|
||||
|
||||
describe("renderChatComposer status", () => {
|
||||
it("swaps the expanded question with the composer and restores its draft and focus", async () => {
|
||||
const container = document.createElement("div");
|
||||
document.body.append(container);
|
||||
const prompt = questionPrompt("question-swap", "Choose a release target");
|
||||
const composerProps = props({
|
||||
paneId: "question-swap-pane",
|
||||
sessionKey: "queue-test",
|
||||
draft: "Keep this draft",
|
||||
gatewayQuestionPrompts: [],
|
||||
composerControls: html`<button type="button">Model</button>`,
|
||||
onRequestUpdate: vi.fn(),
|
||||
});
|
||||
composerProps.onDraftChange = (next) => {
|
||||
composerProps.draft = next;
|
||||
};
|
||||
const draw = () => render(renderChatComposer(composerProps), container);
|
||||
|
||||
draw();
|
||||
const initialTextarea = container.querySelector<HTMLTextAreaElement>("textarea")!;
|
||||
initialTextarea.focus();
|
||||
expect(document.activeElement).toBe(initialTextarea);
|
||||
initialTextarea.dispatchEvent(new CompositionEvent("compositionstart", { bubbles: true }));
|
||||
initialTextarea.value = "Keep this draft while composing";
|
||||
initialTextarea.dispatchEvent(
|
||||
new InputEvent("input", { bubbles: true, inputType: "insertCompositionText" }),
|
||||
);
|
||||
|
||||
composerProps.gatewayQuestionPrompts = [prompt];
|
||||
draw();
|
||||
let panel = container.querySelector("openclaw-chat-question-panel") as HTMLElement & {
|
||||
updateComplete: Promise<unknown>;
|
||||
};
|
||||
await panel.updateComplete;
|
||||
expect(container.querySelector(".agent-chat__input")).toBeNull();
|
||||
expect(container.querySelector(".agent-chat__composer-footer")).toBeNull();
|
||||
expect(document.activeElement).toBe(panel.querySelector(".chat-question-panel"));
|
||||
expect(composerProps.draft).toBe("Keep this draft while composing");
|
||||
|
||||
composerProps.draft = "Host updated this draft while the question was open";
|
||||
|
||||
panel.querySelector<HTMLButtonElement>(".chat-question-panel__collapse")?.click();
|
||||
draw();
|
||||
await Promise.resolve();
|
||||
let textarea = container.querySelector<HTMLTextAreaElement>("textarea")!;
|
||||
expect(textarea.value).toBe("Host updated this draft while the question was open");
|
||||
expect(document.activeElement).toBe(textarea);
|
||||
|
||||
panel = container.querySelector("openclaw-chat-question-panel") as typeof panel;
|
||||
panel.querySelector<HTMLButtonElement>(".chat-question-panel__collapsed-button")?.click();
|
||||
draw();
|
||||
await panel.updateComplete;
|
||||
expect(container.querySelector(".agent-chat__input")).toBeNull();
|
||||
expect(document.activeElement).toBe(panel.querySelector(".chat-question-panel"));
|
||||
|
||||
prompt.status = "answered";
|
||||
draw();
|
||||
await Promise.resolve();
|
||||
textarea = container.querySelector<HTMLTextAreaElement>("textarea")!;
|
||||
expect(textarea.value).toBe("Host updated this draft while the question was open");
|
||||
expect(document.activeElement).toBe(textarea);
|
||||
expect(container.querySelector("openclaw-chat-question-panel")).toBeNull();
|
||||
|
||||
container.remove();
|
||||
});
|
||||
|
||||
it("keeps every concurrent gateway question reachable", async () => {
|
||||
const container = document.createElement("div");
|
||||
const onRequestUpdate = vi.fn();
|
||||
|
||||
@@ -167,6 +167,9 @@ type ChatComposerState = {
|
||||
pendingClearedSubmittedDraft: PendingClearedSubmittedDraft | null;
|
||||
goalExpandedId: string | null;
|
||||
activeGatewayQuestionId: string | null;
|
||||
gatewayQuestionCollapsed: boolean;
|
||||
questionTakeoverActive: boolean;
|
||||
restoreComposerFocus: boolean;
|
||||
composerTextarea: HTMLTextAreaElement | null;
|
||||
// Stable Lit ref: an inline arrow would change identity per render and force
|
||||
// a layout re-measure of the textarea on every chat render, not just attach.
|
||||
@@ -189,6 +192,9 @@ function createChatComposerState(): ChatComposerState {
|
||||
pendingClearedSubmittedDraft: null,
|
||||
goalExpandedId: null,
|
||||
activeGatewayQuestionId: null,
|
||||
gatewayQuestionCollapsed: false,
|
||||
questionTakeoverActive: false,
|
||||
restoreComposerFocus: false,
|
||||
composerTextarea: null,
|
||||
textareaRef: null,
|
||||
};
|
||||
@@ -1933,10 +1939,10 @@ export function renderChatComposer(props: ChatComposerProps) {
|
||||
const compactBusy =
|
||||
props.compactionStatus?.phase === "active" || props.compactionStatus?.phase === "retrying";
|
||||
const activeSession = props.sessions?.sessions?.find((row) => row.key === props.sessionKey);
|
||||
const visibleDraft = props.draft;
|
||||
const draftKey = composerDraftKey(props);
|
||||
const actionDraft =
|
||||
state.composingDraft?.key === draftKey ? state.composingDraft.value : visibleDraft;
|
||||
const visibleDraft =
|
||||
state.composingDraft?.key === draftKey ? state.composingDraft.value : props.draft;
|
||||
const actionDraft = visibleDraft;
|
||||
let questionDock: HTMLElement | null = null;
|
||||
state.textareaRef ??= (element?: Element) => {
|
||||
const nextTextarea = element instanceof HTMLTextAreaElement ? element : null;
|
||||
@@ -1948,6 +1954,10 @@ export function renderChatComposer(props: ChatComposerProps) {
|
||||
if (nextTextarea) {
|
||||
observeTextareaOverflow(nextTextarea);
|
||||
scheduleTextareaHeightAdjustment(nextTextarea);
|
||||
if (state.restoreComposerFocus) {
|
||||
state.restoreComposerFocus = false;
|
||||
queueMicrotask(() => state.composerTextarea?.focus({ preventScroll: true }));
|
||||
}
|
||||
}
|
||||
};
|
||||
// The stable ref only measures on attach, so programmatic draft swaps (send
|
||||
@@ -2005,6 +2015,10 @@ export function renderChatComposer(props: ChatComposerProps) {
|
||||
if (gatewayQuestionIndex < 0 && gatewayQuestionPrompts.length > 0) {
|
||||
gatewayQuestionIndex = 0;
|
||||
state.activeGatewayQuestionId = gatewayQuestionPrompts[0]?.id ?? null;
|
||||
state.gatewayQuestionCollapsed = false;
|
||||
} else if (gatewayQuestionPrompts.length === 0) {
|
||||
state.activeGatewayQuestionId = null;
|
||||
state.gatewayQuestionCollapsed = false;
|
||||
}
|
||||
const gatewayQuestionPrompt = gatewayQuestionPrompts[gatewayQuestionIndex];
|
||||
const selectGatewayQuestion = (index: number) => {
|
||||
@@ -2013,11 +2027,18 @@ export function renderChatComposer(props: ChatComposerProps) {
|
||||
return;
|
||||
}
|
||||
state.activeGatewayQuestionId = prompt.id;
|
||||
state.gatewayQuestionCollapsed = false;
|
||||
requestUpdate();
|
||||
};
|
||||
const questionPanelProps = gatewayQuestionPrompt
|
||||
? createGatewayQuestionPanelProps(gatewayQuestionPrompt, {
|
||||
nowMs: Date.now(),
|
||||
collapsed: state.gatewayQuestionCollapsed,
|
||||
onCollapsedChange: (collapsed) => {
|
||||
state.gatewayQuestionCollapsed = collapsed;
|
||||
state.restoreComposerFocus = collapsed;
|
||||
requestUpdate();
|
||||
},
|
||||
onChange: props.onGatewayQuestionChange,
|
||||
onSubmit: props.onGatewayQuestionSubmit
|
||||
? (answers) => props.onGatewayQuestionSubmit?.(gatewayQuestionPrompt.id, answers)
|
||||
@@ -2038,6 +2059,21 @@ export function renderChatComposer(props: ChatComposerProps) {
|
||||
selectGatewayQuestion((gatewayQuestionIndex + 1) % gatewayQuestionPrompts.length),
|
||||
})
|
||||
: null;
|
||||
const questionTakeoverActive = Boolean(questionPanelProps && !state.gatewayQuestionCollapsed);
|
||||
if (!state.questionTakeoverActive && questionTakeoverActive) {
|
||||
// A question can arrive mid-IME composition before compositionend commits the host draft.
|
||||
// Commit before unmounting so the detached input cannot leave a stale shadow behind.
|
||||
if (state.composingDraft?.key === draftKey) {
|
||||
commitComposerDraft(props, state.composingDraft.value);
|
||||
state.composingDraft = null;
|
||||
}
|
||||
state.composerComposing = false;
|
||||
}
|
||||
if (state.questionTakeoverActive && !questionTakeoverActive) {
|
||||
state.restoreComposerFocus = true;
|
||||
}
|
||||
state.questionTakeoverActive = questionTakeoverActive;
|
||||
const showComposer = !questionTakeoverActive;
|
||||
|
||||
const placeholder =
|
||||
!canCompose && props.disabledReason
|
||||
@@ -2337,175 +2373,183 @@ export function renderChatComposer(props: ChatComposerProps) {
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
<div
|
||||
class="agent-chat__input"
|
||||
@click=${(event: MouseEvent) => focusComposerFromChrome(event, canCompose)}
|
||||
>
|
||||
${slashMenuVisible ? renderSlashMenu(requestUpdate, props, visibleDraft) : nothing}
|
||||
${renderAttachmentPreview(props)}
|
||||
${props.replyTarget
|
||||
? html`
|
||||
<div class="chat-reply-preview">
|
||||
<span class="chat-reply-preview__icon">${icons.messageSquare}</span>
|
||||
<span class="chat-reply-preview__label"
|
||||
>Replying to ${props.replyTarget.senderLabel ?? "message"}</span
|
||||
>
|
||||
<span class="chat-reply-preview__text"
|
||||
>${truncateUtf16Safe(props.replyTarget.text, 120)}${props.replyTarget.text
|
||||
.length > 120
|
||||
? "..."
|
||||
: ""}</span
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="chat-reply-preview__dismiss"
|
||||
@click=${() => props.onClearReply?.()}
|
||||
aria-label=${t("chat.composer.cancelReply")}
|
||||
title=${t("chat.composer.cancelReply")}
|
||||
>
|
||||
${icons.x}
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
<div class="agent-chat__composer-status-stack">
|
||||
${renderChatPlanChecklist(props.planStatus, {
|
||||
active: showAbortableUi,
|
||||
variant: "bar",
|
||||
})}
|
||||
${renderFallbackIndicator(props.fallbackStatus)}
|
||||
${renderCompactionIndicator(props.compactionStatus)}
|
||||
${renderChatGoal(state, activeSession?.goal, {
|
||||
canAct: props.connected && canCompose,
|
||||
onGoalCommand: props.onGoalCommand,
|
||||
onGoalEdit: (goal) => {
|
||||
commitComposerDraft(props, `/goal edit ${goal.objective}`);
|
||||
requestUpdate();
|
||||
queueMicrotask(() => state.composerTextarea?.focus({ preventScroll: true }));
|
||||
},
|
||||
requestUpdate,
|
||||
})}
|
||||
</div>
|
||||
|
||||
${renderChatAttachmentInputs({ ...props, disabled: !canCompose })}
|
||||
${renderChatVoiceError({
|
||||
status: props.realtimeTalkCameraError ? "error" : props.realtimeTalkStatus,
|
||||
detail: props.realtimeTalkDetail,
|
||||
onDismissError: props.realtimeTalkCameraError
|
||||
? undefined
|
||||
: props.onDismissRealtimeTalkError,
|
||||
})}
|
||||
${props.realtimeTalkVideoStream
|
||||
? html`
|
||||
<div class="agent-chat__video-preview">
|
||||
<video
|
||||
autoplay
|
||||
.muted=${true}
|
||||
playsinline
|
||||
aria-label=${t("chat.composer.cameraPreview")}
|
||||
${ref((element) => {
|
||||
if (element instanceof HTMLVideoElement) {
|
||||
element.srcObject = props.realtimeTalkVideoStream ?? null;
|
||||
}
|
||||
})}
|
||||
></video>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${props.disabledReason
|
||||
? html`
|
||||
<div class="agent-chat__disabled-reason">
|
||||
<span>${props.disabledReason}</span>
|
||||
${props.disabledActionLabel && props.onDisabledAction
|
||||
? html`
|
||||
<button type="button" class="btn btn--xs" @click=${props.onDisabledAction}>
|
||||
${props.disabledActionLabel}
|
||||
</button>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<div class="agent-chat__composer-input-row">
|
||||
${renderChatAttachmentMenu({ ...props, disabled: !canCompose })}
|
||||
<div class="agent-chat__composer-combobox">
|
||||
<textarea
|
||||
${ref(state.textareaRef)}
|
||||
.value=${visibleDraft}
|
||||
dir=${detectTextDirection(visibleDraft)}
|
||||
?disabled=${!canCompose}
|
||||
aria-autocomplete="list"
|
||||
aria-controls=${ifDefined(slashMenuVisible ? slashMenuListboxId : undefined)}
|
||||
aria-activedescendant=${ifDefined(activeSlashMenuOptionId ?? undefined)}
|
||||
aria-describedby=${slashMenuAnnouncementId}
|
||||
aria-keyshortcuts=${sendShortcut === "enter" ? "Enter" : "Control+Enter Meta+Enter"}
|
||||
@keydown=${handleKeyDown}
|
||||
@beforeinput=${handleBeforeInput}
|
||||
@input=${handleInput}
|
||||
@compositionstart=${(event: CompositionEvent) => {
|
||||
state.composerComposing = true;
|
||||
state.composingDraft = {
|
||||
key: draftKey,
|
||||
value: (event.target as HTMLTextAreaElement).value,
|
||||
};
|
||||
}}
|
||||
@compositionend=${handleCompositionEnd}
|
||||
@blur=${handleBlur}
|
||||
@paste=${(event: ClipboardEvent) => {
|
||||
if (canCompose) {
|
||||
handleChatAttachmentPaste(event, props);
|
||||
}
|
||||
}}
|
||||
placeholder=${placeholder}
|
||||
rows="1"
|
||||
></textarea>
|
||||
${tokens
|
||||
${showComposer
|
||||
? html`<div
|
||||
class="agent-chat__input"
|
||||
@click=${(event: MouseEvent) => focusComposerFromChrome(event, canCompose)}
|
||||
>
|
||||
${slashMenuVisible ? renderSlashMenu(requestUpdate, props, visibleDraft) : nothing}
|
||||
${renderAttachmentPreview(props)}
|
||||
${props.replyTarget
|
||||
? html`
|
||||
<div class="agent-chat__token-row">
|
||||
<span class="agent-chat__token-count">${tokens}</span>
|
||||
<div class="chat-reply-preview">
|
||||
<span class="chat-reply-preview__icon">${icons.messageSquare}</span>
|
||||
<span class="chat-reply-preview__label"
|
||||
>Replying to ${props.replyTarget.senderLabel ?? "message"}</span
|
||||
>
|
||||
<span class="chat-reply-preview__text"
|
||||
>${truncateUtf16Safe(props.replyTarget.text, 120)}${props.replyTarget.text
|
||||
.length > 120
|
||||
? "..."
|
||||
: ""}</span
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="chat-reply-preview__dismiss"
|
||||
@click=${() => props.onClearReply?.()}
|
||||
aria-label=${t("chat.composer.cancelReply")}
|
||||
title=${t("chat.composer.cancelReply")}
|
||||
>
|
||||
${icons.x}
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
<span
|
||||
id=${slashMenuAnnouncementId}
|
||||
class="agent-chat__sr-only"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
>${activeSlashMenuOptionLabel}</span
|
||||
>
|
||||
<span
|
||||
class="agent-chat__run-status-announcement agent-chat__sr-only"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
>${runStatusAnnouncement}</span
|
||||
>
|
||||
</div>
|
||||
<div class="agent-chat__composer-actions">
|
||||
${renderChatPrimaryActions(runControlsProps)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="agent-chat__composer-status-stack">
|
||||
${renderChatPlanChecklist(props.planStatus, {
|
||||
active: showAbortableUi,
|
||||
variant: "bar",
|
||||
})}
|
||||
${renderFallbackIndicator(props.fallbackStatus)}
|
||||
${renderCompactionIndicator(props.compactionStatus)}
|
||||
${renderChatGoal(state, activeSession?.goal, {
|
||||
canAct: props.connected && canCompose,
|
||||
onGoalCommand: props.onGoalCommand,
|
||||
onGoalEdit: (goal) => {
|
||||
commitComposerDraft(props, `/goal edit ${goal.objective}`);
|
||||
requestUpdate();
|
||||
queueMicrotask(() => state.composerTextarea?.focus({ preventScroll: true }));
|
||||
},
|
||||
requestUpdate,
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div class="agent-chat__composer-footer">
|
||||
${composerControls !== nothing
|
||||
? html`
|
||||
<div class="agent-chat__composer-controls">
|
||||
${composerRunStatus?.phase === "interrupted"
|
||||
? html`
|
||||
<div class="agent-chat__composer-run-status">
|
||||
${renderChatRunStatusIndicator(composerRunStatus)}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${composerControls}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
<div class="agent-chat__composer-meta">${contextNotice}</div>
|
||||
</div>
|
||||
</div>
|
||||
${renderChatAttachmentInputs({ ...props, disabled: !canCompose })}
|
||||
${renderChatVoiceError({
|
||||
status: props.realtimeTalkCameraError ? "error" : props.realtimeTalkStatus,
|
||||
detail: props.realtimeTalkDetail,
|
||||
onDismissError: props.realtimeTalkCameraError
|
||||
? undefined
|
||||
: props.onDismissRealtimeTalkError,
|
||||
})}
|
||||
${props.realtimeTalkVideoStream
|
||||
? html`
|
||||
<div class="agent-chat__video-preview">
|
||||
<video
|
||||
autoplay
|
||||
.muted=${true}
|
||||
playsinline
|
||||
aria-label=${t("chat.composer.cameraPreview")}
|
||||
${ref((element) => {
|
||||
if (element instanceof HTMLVideoElement) {
|
||||
element.srcObject = props.realtimeTalkVideoStream ?? null;
|
||||
}
|
||||
})}
|
||||
></video>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${props.disabledReason
|
||||
? html`
|
||||
<div class="agent-chat__disabled-reason">
|
||||
<span>${props.disabledReason}</span>
|
||||
${props.disabledActionLabel && props.onDisabledAction
|
||||
? html`
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn--xs"
|
||||
@click=${props.onDisabledAction}
|
||||
>
|
||||
${props.disabledActionLabel}
|
||||
</button>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<div class="agent-chat__composer-input-row">
|
||||
${renderChatAttachmentMenu({ ...props, disabled: !canCompose })}
|
||||
<div class="agent-chat__composer-combobox">
|
||||
<textarea
|
||||
${ref(state.textareaRef)}
|
||||
.value=${visibleDraft}
|
||||
dir=${detectTextDirection(visibleDraft)}
|
||||
?disabled=${!canCompose}
|
||||
aria-autocomplete="list"
|
||||
aria-controls=${ifDefined(slashMenuVisible ? slashMenuListboxId : undefined)}
|
||||
aria-activedescendant=${ifDefined(activeSlashMenuOptionId ?? undefined)}
|
||||
aria-describedby=${slashMenuAnnouncementId}
|
||||
aria-keyshortcuts=${sendShortcut === "enter"
|
||||
? "Enter"
|
||||
: "Control+Enter Meta+Enter"}
|
||||
@keydown=${handleKeyDown}
|
||||
@beforeinput=${handleBeforeInput}
|
||||
@input=${handleInput}
|
||||
@compositionstart=${(event: CompositionEvent) => {
|
||||
state.composerComposing = true;
|
||||
state.composingDraft = {
|
||||
key: draftKey,
|
||||
value: (event.target as HTMLTextAreaElement).value,
|
||||
};
|
||||
}}
|
||||
@compositionend=${handleCompositionEnd}
|
||||
@blur=${handleBlur}
|
||||
@paste=${(event: ClipboardEvent) => {
|
||||
if (canCompose) {
|
||||
handleChatAttachmentPaste(event, props);
|
||||
}
|
||||
}}
|
||||
placeholder=${placeholder}
|
||||
rows="1"
|
||||
></textarea>
|
||||
${tokens
|
||||
? html`
|
||||
<div class="agent-chat__token-row">
|
||||
<span class="agent-chat__token-count">${tokens}</span>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
<span
|
||||
id=${slashMenuAnnouncementId}
|
||||
class="agent-chat__sr-only"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
>${activeSlashMenuOptionLabel}</span
|
||||
>
|
||||
<span
|
||||
class="agent-chat__run-status-announcement agent-chat__sr-only"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
>${runStatusAnnouncement}</span
|
||||
>
|
||||
</div>
|
||||
<div class="agent-chat__composer-actions">
|
||||
${renderChatPrimaryActions(runControlsProps)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="agent-chat__composer-footer">
|
||||
${composerControls !== nothing
|
||||
? html`
|
||||
<div class="agent-chat__composer-controls">
|
||||
${composerRunStatus?.phase === "interrupted"
|
||||
? html`
|
||||
<div class="agent-chat__composer-run-status">
|
||||
${renderChatRunStatusIndicator(composerRunStatus)}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${composerControls}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
<div class="agent-chat__composer-meta">${contextNotice}</div>
|
||||
</div>
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -64,11 +64,17 @@ describe("shared question panel", () => {
|
||||
onSkip?: () => void | Promise<void>;
|
||||
} = {},
|
||||
) {
|
||||
let collapsed = false;
|
||||
const redraw = () => {
|
||||
render(
|
||||
html`<openclaw-chat-question-panel
|
||||
.props=${createGatewayQuestionPanelProps(prompt, {
|
||||
nowMs: 2_000,
|
||||
collapsed,
|
||||
onCollapsedChange: (nextCollapsed) => {
|
||||
collapsed = nextCollapsed;
|
||||
redraw();
|
||||
},
|
||||
onChange: redraw,
|
||||
onSubmit: callbacks.onSubmit ?? vi.fn(),
|
||||
onSkip: callbacks.onSkip ?? vi.fn(),
|
||||
@@ -235,6 +241,7 @@ describe("shared question panel", () => {
|
||||
|
||||
container.querySelector<HTMLButtonElement>(".chat-question-panel__collapsed-button")?.click();
|
||||
await panel.updateComplete;
|
||||
expect(document.activeElement).toBe(container.querySelector(".chat-question-panel"));
|
||||
container.querySelector<HTMLButtonElement>(".chat-question-panel__skip")?.click();
|
||||
expect(onSkip).toHaveBeenCalledOnce();
|
||||
});
|
||||
@@ -254,6 +261,24 @@ describe("shared question panel", () => {
|
||||
expect(container.querySelector(".chat-question-panel__skip")).toBeNull();
|
||||
});
|
||||
|
||||
it("manages collapse state when no controlled callback is supplied", async () => {
|
||||
render(
|
||||
html`<openclaw-chat-question-panel
|
||||
.props=${createGatewayQuestionPanelProps(gatewayPrompt(), { nowMs: 2_000 })}
|
||||
></openclaw-chat-question-panel>`,
|
||||
container,
|
||||
);
|
||||
const panel = await panelIn(container);
|
||||
|
||||
container.querySelector<HTMLButtonElement>(".chat-question-panel__collapse")?.click();
|
||||
await panel.updateComplete;
|
||||
expect(container.querySelector(".chat-question-panel--collapsed")).not.toBeNull();
|
||||
|
||||
container.querySelector<HTMLButtonElement>(".chat-question-panel__collapsed-button")?.click();
|
||||
await panel.updateComplete;
|
||||
expect(container.querySelector(".chat-question-panel--collapsed")).toBeNull();
|
||||
});
|
||||
|
||||
it("retains answers with submit-only wiring", async () => {
|
||||
const onSubmit = vi.fn();
|
||||
render(
|
||||
|
||||
@@ -18,6 +18,7 @@ type QuestionPanelViewModel = {
|
||||
requestKey: string;
|
||||
title: string;
|
||||
questions: QuestionPanelQuestion[];
|
||||
collapsed: boolean;
|
||||
disabled: boolean;
|
||||
submitting?: boolean;
|
||||
countdown?: string;
|
||||
@@ -32,6 +33,7 @@ type QuestionPanelProps = {
|
||||
onSkip?: () => void | Promise<void>;
|
||||
onAnswersChange?: (answersById: Record<string, string[]>) => void;
|
||||
onDismissError?: () => void;
|
||||
onCollapsedChange?: (collapsed: boolean) => void;
|
||||
onPreviousRequest?: () => void;
|
||||
onNextRequest?: () => void;
|
||||
};
|
||||
@@ -41,6 +43,8 @@ type GatewayQuestionPanelOptions = {
|
||||
onChange?: () => void;
|
||||
onSubmit?: (answers: Record<string, string[]>) => void | Promise<void>;
|
||||
onSkip?: () => void | Promise<void>;
|
||||
collapsed?: boolean;
|
||||
onCollapsedChange?: (collapsed: boolean) => void;
|
||||
requestPosition?: { current: number; total: number };
|
||||
onPreviousRequest?: () => void;
|
||||
onNextRequest?: () => void;
|
||||
@@ -84,6 +88,7 @@ export function createGatewayQuestionPanelProps(
|
||||
requestKey: prompt.id,
|
||||
title: t("chat.questions.eyebrow"),
|
||||
questions: prompt.questions,
|
||||
collapsed: options.collapsed ?? false,
|
||||
disabled: prompt.status !== "pending" || prompt.submitting,
|
||||
submitting: prompt.submitting,
|
||||
countdown:
|
||||
@@ -121,6 +126,7 @@ export function createGatewayQuestionPanelProps(
|
||||
options.onChange?.();
|
||||
}
|
||||
: undefined,
|
||||
onCollapsedChange: options.onCollapsedChange,
|
||||
onPreviousRequest: options.onPreviousRequest,
|
||||
onNextRequest: options.onNextRequest,
|
||||
};
|
||||
@@ -133,6 +139,9 @@ function terminalAnswer(prompt: QuestionPrompt, question: QuestionPanelQuestion)
|
||||
if (prompt.status === "expired") {
|
||||
return t("chat.questions.expired");
|
||||
}
|
||||
if (prompt.status === "unavailable") {
|
||||
return t("chat.questions.unavailable");
|
||||
}
|
||||
const answer = prompt.answers?.answers[question.id]?.answers.join(", ");
|
||||
if (answer) {
|
||||
return answer;
|
||||
@@ -178,22 +187,40 @@ class ChatQuestionPanel extends LitElement {
|
||||
@state() private selectedById = new Map<string, string[]>();
|
||||
@state() private freeTextById = new Map<string, string>();
|
||||
@state() private currentQuestionIndex = 0;
|
||||
@state() private collapsed = false;
|
||||
@state() private pendingAction: "submit" | "skip" | null = null;
|
||||
private requestKey: string | null = null;
|
||||
private collapsed = false;
|
||||
private focusAfterUpdate = false;
|
||||
private syncedAnswersSignature: string | null = null;
|
||||
|
||||
private setCollapsed(collapsed: boolean): void {
|
||||
if (this.props?.onCollapsedChange) {
|
||||
this.props.onCollapsedChange(collapsed);
|
||||
return;
|
||||
}
|
||||
this.collapsed = collapsed;
|
||||
this.focusAfterUpdate = !collapsed;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
override willUpdate() {
|
||||
const model = this.props?.model;
|
||||
const nextRequestKey = model?.requestKey ?? null;
|
||||
const nextCollapsed = model?.collapsed ?? false;
|
||||
if (nextRequestKey !== this.requestKey) {
|
||||
this.requestKey = nextRequestKey;
|
||||
this.selectedById = new Map();
|
||||
this.freeTextById = new Map();
|
||||
this.currentQuestionIndex = 0;
|
||||
this.collapsed = false;
|
||||
this.pendingAction = null;
|
||||
this.syncedAnswersSignature = null;
|
||||
this.collapsed = nextCollapsed;
|
||||
this.focusAfterUpdate = !nextCollapsed;
|
||||
} else if (this.props?.onCollapsedChange) {
|
||||
if (this.collapsed && !nextCollapsed) {
|
||||
this.focusAfterUpdate = true;
|
||||
}
|
||||
this.collapsed = nextCollapsed;
|
||||
}
|
||||
if (!model?.answersById) {
|
||||
return;
|
||||
@@ -221,6 +248,14 @@ class ChatQuestionPanel extends LitElement {
|
||||
this.freeTextById = freeTextById;
|
||||
}
|
||||
|
||||
override updated(): void {
|
||||
if (!this.focusAfterUpdate || this.collapsed) {
|
||||
return;
|
||||
}
|
||||
this.focusAfterUpdate = false;
|
||||
this.querySelector<HTMLElement>(".chat-question-panel")?.focus({ preventScroll: true });
|
||||
}
|
||||
|
||||
private answerValues(question: QuestionPanelQuestion): string[] {
|
||||
const selected = this.selectedById.get(question.id) ?? [];
|
||||
const freeText = this.freeTextById.get(question.id)?.trim();
|
||||
@@ -441,8 +476,7 @@ class ChatQuestionPanel extends LitElement {
|
||||
class="chat-question-panel__collapsed-button"
|
||||
type="button"
|
||||
@click=${() => {
|
||||
this.collapsed = false;
|
||||
this.focusPanel();
|
||||
this.setCollapsed(false);
|
||||
}}
|
||||
aria-label=${t("chat.questions.expand")}
|
||||
>
|
||||
@@ -504,7 +538,7 @@ class ChatQuestionPanel extends LitElement {
|
||||
<button
|
||||
class="chat-question-panel__collapse"
|
||||
type="button"
|
||||
@click=${() => (this.collapsed = true)}
|
||||
@click=${() => this.setCollapsed(true)}
|
||||
aria-label=${t("chat.questions.collapse")}
|
||||
>
|
||||
${icons.chevronDown}
|
||||
|
||||
Reference in New Issue
Block a user