@jini-ai/ui 0.3.9 → 0.3.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser/useFileDropTarget.js +1 -1
- package/dist/features/execution/react/components/ByokProviderForm.js +3 -3
- package/dist/features/html-editor/canvas-embed-placeholders.d.ts +4 -0
- package/dist/features/html-editor/canvas-embed-placeholders.js +51 -1
- package/dist/features/html-editor/canvas-style.d.ts +11 -0
- package/dist/features/html-editor/canvas-style.js +16 -1
- package/dist/features/html-editor/index.d.ts +1 -1
- package/dist/features/html-editor/react/components/InteractiveHtmlEditor.d.ts +23 -11
- package/dist/features/html-editor/react/components/InteractiveHtmlEditor.js +5 -3
- package/dist/features/html-editor/react/hooks/useInteractiveHtmlEditor.d.ts +11 -1
- package/dist/features/html-editor/react/hooks/useInteractiveHtmlEditor.js +356 -30
- package/dist/features/html-editor/source-splice.d.ts +56 -0
- package/dist/features/html-editor/source-splice.js +89 -0
- package/dist/features/i18n/dictionaries/settings-dialog.ar.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.bn.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.de.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.en.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.es.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.fa.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.fr.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.hi.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.hu.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.id.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.it.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.ja.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.ko.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.pl.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.pt-BR.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.ru.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.th.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.tr.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.uk.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.ur.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.zh-CN.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.zh-TW.js +2 -0
- package/dist/features/mcp-ui/surfaces/bridge.js +1 -0
- package/dist/features/mcp-ui/surfaces/confirmation.d.ts +8 -0
- package/dist/features/mcp-ui/surfaces/confirmation.js +65 -6
- package/dist/features/mcp-ui/surfaces/document.d.ts +11 -1
- package/dist/features/mcp-ui/surfaces/document.js +22 -2
- package/dist/features/mcp-ui/surfaces/form.js +12 -11
- package/package.json +4 -3
|
@@ -48,6 +48,8 @@ export const SETTINGS_DIALOG_EN = {
|
|
|
48
48
|
'Copy': 'Copy',
|
|
49
49
|
'Copy to clipboard': 'Copy to clipboard',
|
|
50
50
|
'Could not load live models: {message}': 'Could not load live models: {message}',
|
|
51
|
+
'No API key — connection test needs the key from this browser.': 'No API key — connection test needs the key from this browser.',
|
|
52
|
+
'No API key — model discovery needs the key from this browser.': 'No API key — model discovery needs the key from this browser.',
|
|
51
53
|
'Could not load skills: {error}': 'Could not load skills: {error}',
|
|
52
54
|
'Could not reach the daemon: {error}': 'Could not reach the daemon: {error}',
|
|
53
55
|
'Could not reach the server. Showing local changes only.': 'Could not reach the server. Showing local changes only.',
|
|
@@ -48,6 +48,8 @@ export const SETTINGS_DIALOG_ES = {
|
|
|
48
48
|
'Copy': 'Copiar',
|
|
49
49
|
'Copy to clipboard': 'Copiar al portapapeles',
|
|
50
50
|
'Could not load live models: {message}': 'No se pudieron cargar los modelos en vivo: {message}',
|
|
51
|
+
'No API key — connection test needs the key from this browser.': 'Sin clave de API: la prueba de conexión necesita la clave desde este navegador.',
|
|
52
|
+
'No API key — model discovery needs the key from this browser.': 'Sin clave de API: la detección de modelos necesita la clave desde este navegador.',
|
|
51
53
|
'Could not load skills: {error}': 'No se pudieron cargar las skills: {error}',
|
|
52
54
|
'Could not reach the daemon: {error}': 'No se pudo contactar con el daemon: {error}',
|
|
53
55
|
'Could not reach the server. Showing local changes only.': 'No se pudo contactar con el servidor. Mostrando solo cambios locales.',
|
|
@@ -48,6 +48,8 @@ export const SETTINGS_DIALOG_FA = {
|
|
|
48
48
|
'Copy': 'کپی',
|
|
49
49
|
'Copy to clipboard': 'کپی در کلیپبورد',
|
|
50
50
|
'Could not load live models: {message}': 'بارگذاری مدلهای زنده ممکن نشد: {message}',
|
|
51
|
+
'No API key — connection test needs the key from this browser.': 'کلید API وجود ندارد — آزمون اتصال به کلید از همین مرورگر نیاز دارد.',
|
|
52
|
+
'No API key — model discovery needs the key from this browser.': 'کلید API وجود ندارد — کشف مدلها به کلید از همین مرورگر نیاز دارد.',
|
|
51
53
|
'Could not load skills: {error}': 'بارگذاری مهارتها ممکن نشد: {error}',
|
|
52
54
|
'Could not reach the daemon: {error}': 'ارتباط با دیمون برقرار نشد: {error}',
|
|
53
55
|
'Could not reach the server. Showing local changes only.': 'ارتباط با سرور برقرار نشد. فقط تغییرات محلی نمایش داده میشود.',
|
|
@@ -48,6 +48,8 @@ export const SETTINGS_DIALOG_FR = {
|
|
|
48
48
|
'Copy': 'Copier',
|
|
49
49
|
'Copy to clipboard': 'Copier dans le presse-papiers',
|
|
50
50
|
'Could not load live models: {message}': 'Impossible de charger les modèles en direct : {message}',
|
|
51
|
+
'No API key — connection test needs the key from this browser.': 'Aucune clé API — le test de connexion nécessite la clé depuis ce navigateur.',
|
|
52
|
+
'No API key — model discovery needs the key from this browser.': 'Aucune clé API — la découverte des modèles nécessite la clé depuis ce navigateur.',
|
|
51
53
|
'Could not load skills: {error}': 'Impossible de charger les compétences : {error}',
|
|
52
54
|
'Could not reach the daemon: {error}': 'Impossible de contacter le daemon : {error}',
|
|
53
55
|
'Could not reach the server. Showing local changes only.': 'Impossible de contacter le serveur. Affichage des modifications locales uniquement.',
|
|
@@ -48,6 +48,8 @@ export const SETTINGS_DIALOG_HI = {
|
|
|
48
48
|
'Copy': 'कॉपी करें',
|
|
49
49
|
'Copy to clipboard': 'क्लिपबोर्ड पर कॉपी करें',
|
|
50
50
|
'Could not load live models: {message}': 'लाइव मॉडल लोड नहीं हो सके: {message}',
|
|
51
|
+
'No API key — connection test needs the key from this browser.': 'कोई API कुंजी नहीं — कनेक्शन परीक्षण के लिए इस ब्राउज़र से कुंजी चाहिए।',
|
|
52
|
+
'No API key — model discovery needs the key from this browser.': 'कोई API कुंजी नहीं — मॉडल खोजने के लिए इस ब्राउज़र से कुंजी चाहिए।',
|
|
51
53
|
'Could not load skills: {error}': 'स्किल्स लोड नहीं हो सकीं: {error}',
|
|
52
54
|
'Could not reach the daemon: {error}': 'डेमन तक नहीं पहुंच सके: {error}',
|
|
53
55
|
'Could not reach the server. Showing local changes only.': 'सर्वर तक नहीं पहुंच सके। केवल स्थानीय बदलाव दिखाए जा रहे हैं।',
|
|
@@ -48,6 +48,8 @@ export const SETTINGS_DIALOG_HU = {
|
|
|
48
48
|
'Copy': 'Másolás',
|
|
49
49
|
'Copy to clipboard': 'Másolás vágólapra',
|
|
50
50
|
'Could not load live models: {message}': 'Nem sikerült betölteni az élő modelleket: {message}',
|
|
51
|
+
'No API key — connection test needs the key from this browser.': 'Nincs API-kulcs — a kapcsolati teszthez a kulcs szükséges ebből a böngészőből.',
|
|
52
|
+
'No API key — model discovery needs the key from this browser.': 'Nincs API-kulcs — a modellek felderítéséhez a kulcs szükséges ebből a böngészőből.',
|
|
51
53
|
'Could not load skills: {error}': 'Nem sikerült betölteni a képességeket: {error}',
|
|
52
54
|
'Could not reach the daemon: {error}': 'Nem sikerült elérni a démont: {error}',
|
|
53
55
|
'Could not reach the server. Showing local changes only.': 'Nem sikerült elérni a szervert. Csak a helyi módosítások jelennek meg.',
|
|
@@ -48,6 +48,8 @@ export const SETTINGS_DIALOG_ID = {
|
|
|
48
48
|
'Copy': 'Salin',
|
|
49
49
|
'Copy to clipboard': 'Salin ke clipboard',
|
|
50
50
|
'Could not load live models: {message}': 'Tidak dapat memuat model live: {message}',
|
|
51
|
+
'No API key — connection test needs the key from this browser.': 'Tidak ada kunci API — uji koneksi memerlukan kunci dari browser ini.',
|
|
52
|
+
'No API key — model discovery needs the key from this browser.': 'Tidak ada kunci API — penemuan model memerlukan kunci dari browser ini.',
|
|
51
53
|
'Could not load skills: {error}': 'Tidak dapat memuat skill: {error}',
|
|
52
54
|
'Could not reach the daemon: {error}': 'Tidak dapat menjangkau daemon: {error}',
|
|
53
55
|
'Could not reach the server. Showing local changes only.': 'Tidak dapat menjangkau server. Hanya menampilkan perubahan lokal.',
|
|
@@ -48,6 +48,8 @@ export const SETTINGS_DIALOG_IT = {
|
|
|
48
48
|
'Copy': 'Copia',
|
|
49
49
|
'Copy to clipboard': 'Copia negli appunti',
|
|
50
50
|
'Could not load live models: {message}': 'Impossibile caricare i modelli in tempo reale: {message}',
|
|
51
|
+
'No API key — connection test needs the key from this browser.': 'Nessuna chiave API — il test di connessione richiede la chiave da questo browser.',
|
|
52
|
+
'No API key — model discovery needs the key from this browser.': 'Nessuna chiave API — il rilevamento dei modelli richiede la chiave da questo browser.',
|
|
51
53
|
'Could not load skills: {error}': 'Impossibile caricare le competenze: {error}',
|
|
52
54
|
'Could not reach the daemon: {error}': 'Impossibile contattare il daemon: {error}',
|
|
53
55
|
'Could not reach the server. Showing local changes only.': 'Impossibile contattare il server. Vengono mostrate solo le modifiche locali.',
|
|
@@ -48,6 +48,8 @@ export const SETTINGS_DIALOG_JA = {
|
|
|
48
48
|
'Copy': 'コピー',
|
|
49
49
|
'Copy to clipboard': 'クリップボードにコピー',
|
|
50
50
|
'Could not load live models: {message}': 'ライブモデルを読み込めませんでした:{message}',
|
|
51
|
+
'No API key — connection test needs the key from this browser.': 'API キーがありません — 接続テストには、このブラウザーからのキーが必要です。',
|
|
52
|
+
'No API key — model discovery needs the key from this browser.': 'API キーがありません — モデルの検出には、このブラウザーからのキーが必要です。',
|
|
51
53
|
'Could not load skills: {error}': 'スキルを読み込めませんでした:{error}',
|
|
52
54
|
'Could not reach the daemon: {error}': 'デーモンに接続できませんでした:{error}',
|
|
53
55
|
'Could not reach the server. Showing local changes only.': 'サーバーに接続できませんでした。ローカルの変更のみ表示しています。',
|
|
@@ -48,6 +48,8 @@ export const SETTINGS_DIALOG_KO = {
|
|
|
48
48
|
'Copy': '복사',
|
|
49
49
|
'Copy to clipboard': '클립보드에 복사',
|
|
50
50
|
'Could not load live models: {message}': '실시간 모델을 불러올 수 없습니다: {message}',
|
|
51
|
+
'No API key — connection test needs the key from this browser.': 'API 키가 없습니다 — 연결 테스트에는 이 브라우저의 키가 필요합니다.',
|
|
52
|
+
'No API key — model discovery needs the key from this browser.': 'API 키가 없습니다 — 모델 검색에는 이 브라우저의 키가 필요합니다.',
|
|
51
53
|
'Could not load skills: {error}': '스킬을 불러올 수 없습니다: {error}',
|
|
52
54
|
'Could not reach the daemon: {error}': '데몬에 연결할 수 없습니다: {error}',
|
|
53
55
|
'Could not reach the server. Showing local changes only.': '서버에 연결할 수 없습니다. 로컬 변경 사항만 표시합니다.',
|
|
@@ -48,6 +48,8 @@ export const SETTINGS_DIALOG_PL = {
|
|
|
48
48
|
'Copy': 'Kopiuj',
|
|
49
49
|
'Copy to clipboard': 'Kopiuj do schowka',
|
|
50
50
|
'Could not load live models: {message}': 'Nie udało się wczytać modeli na żywo: {message}',
|
|
51
|
+
'No API key — connection test needs the key from this browser.': 'Brak klucza API — test połączenia wymaga klucza z tej przeglądarki.',
|
|
52
|
+
'No API key — model discovery needs the key from this browser.': 'Brak klucza API — wykrywanie modeli wymaga klucza z tej przeglądarki.',
|
|
51
53
|
'Could not load skills: {error}': 'Nie udało się wczytać umiejętności: {error}',
|
|
52
54
|
'Could not reach the daemon: {error}': 'Nie udało się połączyć z demonem: {error}',
|
|
53
55
|
'Could not reach the server. Showing local changes only.': 'Nie udało się połączyć z serwerem. Wyświetlane są tylko zmiany lokalne.',
|
|
@@ -48,6 +48,8 @@ export const SETTINGS_DIALOG_PT_BR = {
|
|
|
48
48
|
'Copy': 'Copiar',
|
|
49
49
|
'Copy to clipboard': 'Copiar para a área de transferência',
|
|
50
50
|
'Could not load live models: {message}': 'Não foi possível carregar os modelos em tempo real: {message}',
|
|
51
|
+
'No API key — connection test needs the key from this browser.': 'Sem chave de API — o teste de conexão precisa da chave deste navegador.',
|
|
52
|
+
'No API key — model discovery needs the key from this browser.': 'Sem chave de API — a descoberta de modelos precisa da chave deste navegador.',
|
|
51
53
|
'Could not load skills: {error}': 'Não foi possível carregar as skills: {error}',
|
|
52
54
|
'Could not reach the daemon: {error}': 'Não foi possível contatar o daemon: {error}',
|
|
53
55
|
'Could not reach the server. Showing local changes only.': 'Não foi possível contatar o servidor. Mostrando apenas as alterações locais.',
|
|
@@ -48,6 +48,8 @@ export const SETTINGS_DIALOG_RU = {
|
|
|
48
48
|
'Copy': 'Копировать',
|
|
49
49
|
'Copy to clipboard': 'Копировать в буфер обмена',
|
|
50
50
|
'Could not load live models: {message}': 'Не удалось загрузить актуальные модели: {message}',
|
|
51
|
+
'No API key — connection test needs the key from this browser.': 'Нет API-ключа — для проверки подключения нужен ключ из этого браузера.',
|
|
52
|
+
'No API key — model discovery needs the key from this browser.': 'Нет API-ключа — для поиска моделей нужен ключ из этого браузера.',
|
|
51
53
|
'Could not load skills: {error}': 'Не удалось загрузить навыки: {error}',
|
|
52
54
|
'Could not reach the daemon: {error}': 'Не удалось связаться с демоном: {error}',
|
|
53
55
|
'Could not reach the server. Showing local changes only.': 'Не удалось связаться с сервером. Показаны только локальные изменения.',
|
|
@@ -48,6 +48,8 @@ export const SETTINGS_DIALOG_TH = {
|
|
|
48
48
|
'Copy': 'คัดลอก',
|
|
49
49
|
'Copy to clipboard': 'คัดลอกไปยังคลิปบอร์ด',
|
|
50
50
|
'Could not load live models: {message}': 'ไม่สามารถโหลดโมเดลแบบเรียลไทม์ได้: {message}',
|
|
51
|
+
'No API key — connection test needs the key from this browser.': 'ไม่มีคีย์ API — การทดสอบการเชื่อมต่อต้องใช้คีย์จากเบราว์เซอร์นี้',
|
|
52
|
+
'No API key — model discovery needs the key from this browser.': 'ไม่มีคีย์ API — การค้นหาโมเดลต้องใช้คีย์จากเบราว์เซอร์นี้',
|
|
51
53
|
'Could not load skills: {error}': 'ไม่สามารถโหลดทักษะได้: {error}',
|
|
52
54
|
'Could not reach the daemon: {error}': 'ไม่สามารถติดต่อดีมอนได้: {error}',
|
|
53
55
|
'Could not reach the server. Showing local changes only.': 'ไม่สามารถติดต่อเซิร์ฟเวอร์ได้ กำลังแสดงเฉพาะการเปลี่ยนแปลงในเครื่อง',
|
|
@@ -48,6 +48,8 @@ export const SETTINGS_DIALOG_TR = {
|
|
|
48
48
|
'Copy': 'Kopyala',
|
|
49
49
|
'Copy to clipboard': 'Panoya kopyala',
|
|
50
50
|
'Could not load live models: {message}': 'Canlı modeller yüklenemedi: {message}',
|
|
51
|
+
'No API key — connection test needs the key from this browser.': 'API anahtarı yok — bağlantı testi için bu tarayıcıdan anahtar gerekiyor.',
|
|
52
|
+
'No API key — model discovery needs the key from this browser.': 'API anahtarı yok — model keşfi için bu tarayıcıdan anahtar gerekiyor.',
|
|
51
53
|
'Could not load skills: {error}': 'Beceriler yüklenemedi: {error}',
|
|
52
54
|
'Could not reach the daemon: {error}': 'Arka plan sürecine ulaşılamadı: {error}',
|
|
53
55
|
'Could not reach the server. Showing local changes only.': 'Sunucuya ulaşılamadı. Yalnızca yerel değişiklikler gösteriliyor.',
|
|
@@ -48,6 +48,8 @@ export const SETTINGS_DIALOG_UK = {
|
|
|
48
48
|
'Copy': 'Копіювати',
|
|
49
49
|
'Copy to clipboard': 'Копіювати в буфер обміну',
|
|
50
50
|
'Could not load live models: {message}': 'Не вдалося завантажити актуальні моделі: {message}',
|
|
51
|
+
'No API key — connection test needs the key from this browser.': 'Немає API-ключа — для перевірки підключення потрібен ключ із цього браузера.',
|
|
52
|
+
'No API key — model discovery needs the key from this browser.': 'Немає API-ключа — для пошуку моделей потрібен ключ із цього браузера.',
|
|
51
53
|
'Could not load skills: {error}': 'Не вдалося завантажити навички: {error}',
|
|
52
54
|
'Could not reach the daemon: {error}': 'Не вдалося з\'єднатися з демоном: {error}',
|
|
53
55
|
'Could not reach the server. Showing local changes only.': 'Не вдалося з\'єднатися з сервером. Показано лише локальні зміни.',
|
|
@@ -48,6 +48,8 @@ export const SETTINGS_DIALOG_UR = {
|
|
|
48
48
|
'Copy': 'کاپی کریں',
|
|
49
49
|
'Copy to clipboard': 'کلپ بورڈ پر کاپی کریں',
|
|
50
50
|
'Could not load live models: {message}': 'لائیو ماڈلز لوڈ نہیں ہو سکے: {message}',
|
|
51
|
+
'No API key — connection test needs the key from this browser.': 'کوئی API کلید نہیں — کنکشن ٹیسٹ کے لیے اسی براؤزر سے کلید درکار ہے۔',
|
|
52
|
+
'No API key — model discovery needs the key from this browser.': 'کوئی API کلید نہیں — ماڈلز کی دریافت کے لیے اسی براؤزر سے کلید درکار ہے۔',
|
|
51
53
|
'Could not load skills: {error}': 'اسکلز لوڈ نہیں ہو سکیں: {error}',
|
|
52
54
|
'Could not reach the daemon: {error}': 'ڈیمن تک رسائی نہیں ہو سکی: {error}',
|
|
53
55
|
'Could not reach the server. Showing local changes only.': 'سرور تک رسائی نہیں ہو سکی۔ صرف مقامی تبدیلیاں دکھائی جا رہی ہیں۔',
|
|
@@ -48,6 +48,8 @@ export const SETTINGS_DIALOG_ZH_CN = {
|
|
|
48
48
|
'Copy': '复制',
|
|
49
49
|
'Copy to clipboard': '复制到剪贴板',
|
|
50
50
|
'Could not load live models: {message}': '无法加载实时模型列表:{message}',
|
|
51
|
+
'No API key — connection test needs the key from this browser.': '没有 API 密钥——连接测试需要来自此浏览器的密钥。',
|
|
52
|
+
'No API key — model discovery needs the key from this browser.': '没有 API 密钥——模型发现需要来自此浏览器的密钥。',
|
|
51
53
|
'Could not load skills: {error}': '无法加载技能:{error}',
|
|
52
54
|
'Could not reach the daemon: {error}': '无法连接到守护进程:{error}',
|
|
53
55
|
'Could not reach the server. Showing local changes only.': '无法连接到服务器,仅显示本地更改。',
|
|
@@ -48,6 +48,8 @@ export const SETTINGS_DIALOG_ZH_TW = {
|
|
|
48
48
|
'Copy': '複製',
|
|
49
49
|
'Copy to clipboard': '複製到剪貼簿',
|
|
50
50
|
'Could not load live models: {message}': '無法載入即時模型清單:{message}',
|
|
51
|
+
'No API key — connection test needs the key from this browser.': '沒有 API 金鑰——連線測試需要來自此瀏覽器的金鑰。',
|
|
52
|
+
'No API key — model discovery needs the key from this browser.': '沒有 API 金鑰——模型探索需要來自此瀏覽器的金鑰。',
|
|
51
53
|
'Could not load skills: {error}': '無法載入技能:{error}',
|
|
52
54
|
'Could not reach the daemon: {error}': '無法連上背景程序:{error}',
|
|
53
55
|
'Could not reach the server. Showing local changes only.': '無法連上伺服器,僅顯示本機變更。',
|
|
@@ -150,6 +150,7 @@ export function renderBridgeScript(spec) {
|
|
|
150
150
|
function toError(error) {
|
|
151
151
|
var failure = new Error(error && typeof error.message === "string" ? error.message : "Host returned an error");
|
|
152
152
|
if (error && typeof error.code === "number") failure.code = error.code;
|
|
153
|
+
if (error && error.data !== undefined) failure.data = error.data;
|
|
153
154
|
return failure;
|
|
154
155
|
}
|
|
155
156
|
|
|
@@ -59,6 +59,14 @@ export interface ConfirmationSurfaceSpec {
|
|
|
59
59
|
readonly lang?: string;
|
|
60
60
|
readonly tokens?: Partial<Record<SurfaceTokenName, string>>;
|
|
61
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* How long the dialog must have been visible before a confirm click counts, in ms. A click sooner
|
|
64
|
+
* than a person could have read the dialog is ignored — the same dwell browsers put on permission
|
|
65
|
+
* prompts against clickjacking, and a guard against an automated click racing the render. Longer
|
|
66
|
+
* than the ~1 s render-to-confirm gap of the incident it was added for. The confirm button is
|
|
67
|
+
* rendered disabled and enabled when the dwell ends, so a person sees why an early click does nothing.
|
|
68
|
+
*/
|
|
69
|
+
export declare const CONFIRM_DWELL_MS = 1500;
|
|
62
70
|
export declare function renderConfirmationDocument(spec: ConfirmationSurfaceSpec): string;
|
|
63
71
|
/**
|
|
64
72
|
* Renders the confirmation dialog as an MCP-UI resource, ready for a tool result's `content` array.
|
|
@@ -30,6 +30,14 @@
|
|
|
30
30
|
import { createUIResource } from '../resource.js';
|
|
31
31
|
import { escapeHtml, escapeJsValue } from '../escape.js';
|
|
32
32
|
import { DEFAULT_SURFACE_STATUS_TEXT, SURFACE_SCRIPT_PRELUDE, renderActions, renderDetailList, renderStatusRegion, renderSurfaceDocument, renderSurfaceHeader, } from './document.js';
|
|
33
|
+
/**
|
|
34
|
+
* How long the dialog must have been visible before a confirm click counts, in ms. A click sooner
|
|
35
|
+
* than a person could have read the dialog is ignored — the same dwell browsers put on permission
|
|
36
|
+
* prompts against clickjacking, and a guard against an automated click racing the render. Longer
|
|
37
|
+
* than the ~1 s render-to-confirm gap of the incident it was added for. The confirm button is
|
|
38
|
+
* rendered disabled and enabled when the dwell ends, so a person sees why an early click does nothing.
|
|
39
|
+
*/
|
|
40
|
+
export const CONFIRM_DWELL_MS = 1500;
|
|
33
41
|
const DEFAULT_APP = { appName: 'jini-mcp-ui-confirmation', appVersion: '1' };
|
|
34
42
|
function isToolAction(action) {
|
|
35
43
|
return action !== undefined && 'toolName' in action;
|
|
@@ -65,7 +73,8 @@ export function renderConfirmationDocument(spec) {
|
|
|
65
73
|
renderSurfaceHeader(spec.description === undefined ? { title: spec.title } : { title: spec.title, description: spec.description }),
|
|
66
74
|
renderDetailList(spec.details ?? []),
|
|
67
75
|
warning,
|
|
68
|
-
|
|
76
|
+
// Confirm starts disabled; the script enables it once the dwell has run out.
|
|
77
|
+
renderActions(actions.map((action) => (action.id === 'confirm' ? { ...action, disabled: true } : action))),
|
|
69
78
|
renderStatusRegion(),
|
|
70
79
|
]
|
|
71
80
|
.filter((fragment) => fragment !== '')
|
|
@@ -86,14 +95,62 @@ export function renderConfirmationDocument(spec) {
|
|
|
86
95
|
${SURFACE_SCRIPT_PRELUDE}
|
|
87
96
|
var PLAN = ${escapeJsValue(plan)};
|
|
88
97
|
var TEXT = ${escapeJsValue(text)};
|
|
98
|
+
var DWELL_MS = ${CONFIRM_DWELL_MS};
|
|
99
|
+
|
|
100
|
+
// Monotonic where available: a wall clock set backwards would otherwise stretch the dwell.
|
|
101
|
+
function now() {
|
|
102
|
+
return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
|
|
103
|
+
}
|
|
104
|
+
// Set while a call is in flight and for good once one settles the dialog. The disabled buttons
|
|
105
|
+
// already block clicks in a browser; this makes the guarantee not depend on the DOM honoring that.
|
|
106
|
+
var locked = false;
|
|
107
|
+
|
|
108
|
+
// When the dialog last became visible; null while hidden. A dialog that loads in a background tab
|
|
109
|
+
// has not been seen, so its dwell starts when it is shown, and restarts each time it is shown again.
|
|
110
|
+
var visibleSince = null;
|
|
111
|
+
var dwellTimer = null;
|
|
112
|
+
var confirmButton = document.querySelector('[data-mcpui-action="confirm"]');
|
|
113
|
+
|
|
114
|
+
function dwellDone() {
|
|
115
|
+
return visibleSince !== null && now() - visibleSince >= DWELL_MS;
|
|
116
|
+
}
|
|
117
|
+
// Confirm is enabled exactly when the dwell has run out and no call holds the dialog.
|
|
118
|
+
function syncConfirm() {
|
|
119
|
+
if (confirmButton !== null && !locked) confirmButton.disabled = !dwellDone();
|
|
120
|
+
}
|
|
121
|
+
// Re-checks rather than trusts the timer: setTimeout and now() are different clocks, and a timer
|
|
122
|
+
// that fired a hair early must not leave confirm disabled for good.
|
|
123
|
+
function onDwellTimer() {
|
|
124
|
+
dwellTimer = null;
|
|
125
|
+
if (visibleSince === null) return;
|
|
126
|
+
var left = DWELL_MS - (now() - visibleSince);
|
|
127
|
+
if (left > 0) dwellTimer = setTimeout(onDwellTimer, left);
|
|
128
|
+
else syncConfirm();
|
|
129
|
+
}
|
|
130
|
+
function onVisibilityChange() {
|
|
131
|
+
if (dwellTimer !== null) clearTimeout(dwellTimer);
|
|
132
|
+
dwellTimer = null;
|
|
133
|
+
visibleSince = document.visibilityState === "hidden" ? null : now();
|
|
134
|
+
syncConfirm();
|
|
135
|
+
if (visibleSince !== null) dwellTimer = setTimeout(onDwellTimer, DWELL_MS);
|
|
136
|
+
}
|
|
137
|
+
onVisibilityChange();
|
|
138
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
89
139
|
|
|
90
140
|
for (var i = 0; i < actionButtons.length; i++) {
|
|
91
141
|
actionButtons[i].addEventListener("click", onClick);
|
|
92
142
|
}
|
|
93
143
|
|
|
94
144
|
function onClick(event) {
|
|
95
|
-
|
|
145
|
+
// Only a click the browser attributes to the user counts. element.click() and dispatchEvent
|
|
146
|
+
// from any script in this document produce isTrusted === false.
|
|
147
|
+
if (event.isTrusted !== true || locked) return;
|
|
148
|
+
var action = event.currentTarget.getAttribute("data-mcpui-action");
|
|
149
|
+
var step = PLAN[action];
|
|
96
150
|
if (step === undefined) return;
|
|
151
|
+
// Cancel is exempt: backing out early is never the harm this guards against.
|
|
152
|
+
if (action === "confirm" && !dwellDone()) return;
|
|
153
|
+
locked = true;
|
|
97
154
|
if (step === null) {
|
|
98
155
|
setBusy(true);
|
|
99
156
|
setStatus(TEXT.dismissed, "dismissed");
|
|
@@ -106,10 +163,12 @@ ${SURFACE_SCRIPT_PRELUDE}
|
|
|
106
163
|
setStatus(TEXT.done, "done");
|
|
107
164
|
api.requestTeardown();
|
|
108
165
|
}, function (error) {
|
|
109
|
-
// Re-enabled on failure
|
|
110
|
-
// or cancel
|
|
111
|
-
|
|
112
|
-
|
|
166
|
+
// Re-enabled on failure (a rejected call did not happen, so the human must be able to retry
|
|
167
|
+
// or cancel), unless the Host says this dialog is no longer pending -- see reportCallFailure.
|
|
168
|
+
if (!reportCallFailure(error)) return;
|
|
169
|
+
locked = false;
|
|
170
|
+
// reportCallFailure re-enabled every button; confirm still waits out an unfinished dwell.
|
|
171
|
+
syncConfirm();
|
|
113
172
|
});
|
|
114
173
|
}
|
|
115
174
|
}());`;
|
|
@@ -8,7 +8,7 @@ import { type SurfaceTokenName } from './tokens.js';
|
|
|
8
8
|
* be a color no caller could reskin. `var(--x, fallback)` appears only where a value is genuinely
|
|
9
9
|
* optional.
|
|
10
10
|
*/
|
|
11
|
-
export declare const SURFACE_BASE_CSS = "*, *::before, *::after { box-sizing: border-box; }\nhtml, body { margin: 0; padding: 0; }\nbody {\n /* Transparent, not var(--jini-mcpui-bg): this document has no page of its own to paint. It is an\n allow-scripts-only sandboxed iframe embedded directly in a host's chat transcript, and html's\n background is left at its (also transparent) initial value, so the host's own pane shows\n straight through to .mcpui-surface below rather than through a second, unrelated rectangle of\n fill color sitting between the two. --jini-mcpui-bg stays declared for a caller styling\n something else via extraCss -- nothing in this stylesheet reads it anymore. */\n background: transparent;\n color: var(--jini-mcpui-text);\n font-family: var(--jini-mcpui-font);\n font-size: 14px;\n line-height: 1.5;\n padding: 16px;\n -webkit-font-smoothing: antialiased;\n}\n.mcpui-surface {\n background: linear-gradient(135deg,\n color-mix(in srgb, var(--jini-mcpui-accent) 5%, var(--jini-mcpui-panel)) 0%,\n var(--jini-mcpui-panel) 60%);\n border: 1px solid color-mix(in srgb, var(--jini-mcpui-accent) 18%, var(--jini-mcpui-border));\n border-radius: var(--jini-mcpui-radius-md);\n box-shadow: var(--jini-mcpui-surface-shadow);\n padding: 18px 20px 16px;\n max-width: 640px;\n margin: 0 auto;\n}\n.mcpui-title {\n margin: 0;\n font-size: 16px;\n font-weight: 600;\n letter-spacing: -0.01em;\n color: var(--jini-mcpui-text-strong);\n}\n.mcpui-description { margin: 6px 0 0; color: var(--jini-mcpui-text-muted); }\n.mcpui-details {\n display: grid;\n grid-template-columns: max-content minmax(0, 1fr);\n gap: 4px 14px;\n margin: 14px 0 0;\n}\n.mcpui-details dt { color: var(--jini-mcpui-text-soft); font-weight: 500; }\n.mcpui-details dd { margin: 0; overflow-wrap: anywhere; color: var(--jini-mcpui-text); }\n.mcpui-warning {\n margin: 14px 0 0;\n padding: 9px 11px;\n border-radius: var(--jini-mcpui-radius-md);\n border: 1px solid var(--jini-mcpui-danger);\n background: var(--jini-mcpui-danger-tint);\n color: var(--jini-mcpui-text-strong);\n}\n.mcpui-fields { display: flex; flex-direction: column; gap: 12px; margin: 16px 0 0; border: 0; padding: 0; }\n.mcpui-field { display: flex; flex-direction: column; gap: 4px; }\n.mcpui-field-inline { flex-direction: row; align-items: flex-start; gap: 8px; }\n.mcpui-label { font-weight: 500; color: var(--jini-mcpui-text-strong); }\n.mcpui-required { color: var(--jini-mcpui-accent); margin-left: 2px; }\n.mcpui-hint { color: var(--jini-mcpui-text-soft); font-size: 13px; }\n.mcpui-input, .mcpui-select, .mcpui-textarea {\n font: inherit;\n color: var(--jini-mcpui-text);\n background: var(--jini-mcpui-panel);\n border: 1px solid color-mix(in srgb, var(--jini-mcpui-accent) 14%, var(--jini-mcpui-border-strong));\n border-radius: var(--jini-mcpui-radius-sm);\n padding: 7px 9px;\n width: 100%;\n min-width: 0;\n}\n.mcpui-textarea { resize: vertical; min-height: 72px; font-family: var(--jini-mcpui-font); }\n.mcpui-checkbox { margin: 3px 0 0; accent-color: var(--jini-mcpui-accent); }\n.mcpui-choice-group { border: 0; padding: 0; margin: 0; min-width: 0; }\n.mcpui-choice { display: flex; flex-direction: row; align-items: flex-start; gap: 8px; margin: 6px 0 0; }\n.mcpui-choice-label { color: var(--jini-mcpui-text); font-weight: 400; }\n.mcpui-input:focus-visible, .mcpui-select:focus-visible, .mcpui-textarea:focus-visible,\n.mcpui-checkbox:focus-visible, .mcpui-button:focus-visible {\n outline: 2px solid var(--jini-mcpui-accent-ring);\n outline-offset: 1px;\n border-color: var(--jini-mcpui-accent);\n}\n.mcpui-actions { display: flex; flex-wrap: wrap; gap: 8px; margin: 18px 0 0; }\n.mcpui-button {\n font: inherit;\n font-weight: 500;\n padding: 7px 15px;\n border-radius: var(--jini-mcpui-radius-pill);\n border: 1px solid color-mix(in srgb, var(--jini-mcpui-accent) 14%, var(--jini-mcpui-border-strong));\n background: var(--jini-mcpui-panel);\n color: var(--jini-mcpui-text);\n cursor: pointer;\n}\n.mcpui-button:hover:not([disabled]) { background: var(--jini-mcpui-accent-tint); }\n.mcpui-button-primary {\n background: var(--jini-mcpui-accent);\n border-color: var(--jini-mcpui-accent);\n color: var(--jini-mcpui-panel);\n}\n.mcpui-button-primary:hover:not([disabled]) { background: var(--jini-mcpui-accent-hover); border-color: var(--jini-mcpui-accent-hover); }\n.mcpui-button-danger {\n background: var(--jini-mcpui-danger);\n border-color: var(--jini-mcpui-danger);\n color: var(--jini-mcpui-panel);\n}\n.mcpui-button-danger:hover:not([disabled]) { background: var(--jini-mcpui-danger-hover); border-color: var(--jini-mcpui-danger-hover); }\n.mcpui-button[disabled] { opacity: 0.55; cursor: default; }\n.mcpui-status { margin: 12px 0 0; min-height: 1.4em; color: var(--jini-mcpui-text-muted); }\n.mcpui-status[data-state=\"failed\"], .mcpui-status[data-state=\"invalid\"] { color: var(--jini-mcpui-danger); }\n.mcpui-status[data-state=\"done\"] { color: var(--jini-mcpui-text-strong); }\n/* Neither a plain success nor a failure \u2014 e.g. an outcome surface reporting \"uploaded, not yet\n reachable\" (outcome.ts's own state: partial). Distinct from both --jini-mcpui-danger (would\n wrongly read as \"this failed\") and --jini-mcpui-text-strong (would wrongly read as \"this is\n fully done\"); the accent token reads as \"worth a second look\" without asserting either. */\n.mcpui-status[data-state=\"partial\"] { color: var(--jini-mcpui-accent); }\n.mcpui-code { font-family: var(--jini-mcpui-font-mono); font-size: 13px; color: var(--jini-mcpui-text-faint); }";
|
|
11
|
+
export declare const SURFACE_BASE_CSS = "*, *::before, *::after { box-sizing: border-box; }\nhtml, body { margin: 0; padding: 0; }\nbody {\n /* Transparent, not var(--jini-mcpui-bg): this document has no page of its own to paint. It is an\n allow-scripts-only sandboxed iframe embedded directly in a host's chat transcript, and html's\n background is left at its (also transparent) initial value, so the host's own pane shows\n straight through to .mcpui-surface below rather than through a second, unrelated rectangle of\n fill color sitting between the two. --jini-mcpui-bg stays declared for a caller styling\n something else via extraCss -- nothing in this stylesheet reads it anymore. */\n background: transparent;\n color: var(--jini-mcpui-text);\n font-family: var(--jini-mcpui-font);\n font-size: 14px;\n line-height: 1.5;\n padding: 16px;\n -webkit-font-smoothing: antialiased;\n}\n.mcpui-surface {\n background: linear-gradient(135deg,\n color-mix(in srgb, var(--jini-mcpui-accent) 5%, var(--jini-mcpui-panel)) 0%,\n var(--jini-mcpui-panel) 60%);\n border: 1px solid color-mix(in srgb, var(--jini-mcpui-accent) 18%, var(--jini-mcpui-border));\n border-radius: var(--jini-mcpui-radius-md);\n box-shadow: var(--jini-mcpui-surface-shadow);\n padding: 18px 20px 16px;\n max-width: 640px;\n margin: 0 auto;\n}\n.mcpui-title {\n margin: 0;\n font-size: 16px;\n font-weight: 600;\n letter-spacing: -0.01em;\n color: var(--jini-mcpui-text-strong);\n}\n.mcpui-description { margin: 6px 0 0; color: var(--jini-mcpui-text-muted); }\n.mcpui-details {\n display: grid;\n grid-template-columns: max-content minmax(0, 1fr);\n gap: 4px 14px;\n margin: 14px 0 0;\n}\n.mcpui-details dt { color: var(--jini-mcpui-text-soft); font-weight: 500; }\n.mcpui-details dd { margin: 0; overflow-wrap: anywhere; color: var(--jini-mcpui-text); }\n.mcpui-warning {\n margin: 14px 0 0;\n padding: 9px 11px;\n border-radius: var(--jini-mcpui-radius-md);\n border: 1px solid var(--jini-mcpui-danger);\n background: var(--jini-mcpui-danger-tint);\n color: var(--jini-mcpui-text-strong);\n}\n.mcpui-fields { display: flex; flex-direction: column; gap: 12px; margin: 16px 0 0; border: 0; padding: 0; }\n.mcpui-field { display: flex; flex-direction: column; gap: 4px; }\n.mcpui-field-inline { flex-direction: row; align-items: flex-start; gap: 8px; }\n.mcpui-label { font-weight: 500; color: var(--jini-mcpui-text-strong); }\n.mcpui-required { color: var(--jini-mcpui-accent); margin-left: 2px; }\n.mcpui-hint { color: var(--jini-mcpui-text-soft); font-size: 13px; }\n.mcpui-input, .mcpui-select, .mcpui-textarea {\n font: inherit;\n color: var(--jini-mcpui-text);\n background: var(--jini-mcpui-panel);\n border: 1px solid color-mix(in srgb, var(--jini-mcpui-accent) 14%, var(--jini-mcpui-border-strong));\n border-radius: var(--jini-mcpui-radius-sm);\n padding: 7px 9px;\n width: 100%;\n min-width: 0;\n}\n.mcpui-textarea { resize: vertical; min-height: 72px; font-family: var(--jini-mcpui-font); }\n.mcpui-checkbox { margin: 3px 0 0; accent-color: var(--jini-mcpui-accent); }\n.mcpui-choice-group { border: 0; padding: 0; margin: 0; min-width: 0; }\n.mcpui-choice { display: flex; flex-direction: row; align-items: flex-start; gap: 8px; margin: 6px 0 0; }\n.mcpui-choice-label { color: var(--jini-mcpui-text); font-weight: 400; }\n.mcpui-input:focus-visible, .mcpui-select:focus-visible, .mcpui-textarea:focus-visible,\n.mcpui-checkbox:focus-visible, .mcpui-button:focus-visible {\n outline: 2px solid var(--jini-mcpui-accent-ring);\n outline-offset: 1px;\n border-color: var(--jini-mcpui-accent);\n}\n.mcpui-actions { display: flex; flex-wrap: wrap; gap: 8px; margin: 18px 0 0; }\n.mcpui-button {\n font: inherit;\n font-weight: 500;\n padding: 7px 15px;\n border-radius: var(--jini-mcpui-radius-pill);\n border: 1px solid color-mix(in srgb, var(--jini-mcpui-accent) 14%, var(--jini-mcpui-border-strong));\n background: var(--jini-mcpui-panel);\n color: var(--jini-mcpui-text);\n cursor: pointer;\n}\n.mcpui-button:hover:not([disabled]) { background: var(--jini-mcpui-accent-tint); }\n.mcpui-button-primary {\n background: var(--jini-mcpui-accent);\n border-color: var(--jini-mcpui-accent);\n color: var(--jini-mcpui-panel);\n}\n.mcpui-button-primary:hover:not([disabled]) { background: var(--jini-mcpui-accent-hover); border-color: var(--jini-mcpui-accent-hover); }\n.mcpui-button-danger {\n background: var(--jini-mcpui-danger);\n border-color: var(--jini-mcpui-danger);\n color: var(--jini-mcpui-panel);\n}\n.mcpui-button-danger:hover:not([disabled]) { background: var(--jini-mcpui-danger-hover); border-color: var(--jini-mcpui-danger-hover); }\n.mcpui-button[disabled] { opacity: 0.55; cursor: default; }\n.mcpui-status { margin: 12px 0 0; min-height: 1.4em; color: var(--jini-mcpui-text-muted); }\n.mcpui-status[data-state=\"failed\"], .mcpui-status[data-state=\"invalid\"], .mcpui-status[data-state=\"expired\"] { color: var(--jini-mcpui-danger); }\n.mcpui-status[data-state=\"done\"] { color: var(--jini-mcpui-text-strong); }\n/* Neither a plain success nor a failure \u2014 e.g. an outcome surface reporting \"uploaded, not yet\n reachable\" (outcome.ts's own state: partial). Distinct from both --jini-mcpui-danger (would\n wrongly read as \"this failed\") and --jini-mcpui-text-strong (would wrongly read as \"this is\n fully done\"); the accent token reads as \"worth a second look\" without asserting either. */\n.mcpui-status[data-state=\"partial\"] { color: var(--jini-mcpui-accent); }\n.mcpui-code { font-family: var(--jini-mcpui-font-mono); font-size: 13px; color: var(--jini-mcpui-text-faint); }";
|
|
12
12
|
/**
|
|
13
13
|
* The Content-Security-Policy every generated surface declares in its own `<head>`.
|
|
14
14
|
*
|
|
@@ -76,6 +76,12 @@ export interface SurfaceDetail {
|
|
|
76
76
|
export declare function renderDetailList(details: readonly SurfaceDetail[]): string;
|
|
77
77
|
/** Renders the polite live region every surface reports outcomes through. Its id is fixed so surface scripts can find it. */
|
|
78
78
|
export declare function renderStatusRegion(): string;
|
|
79
|
+
/**
|
|
80
|
+
* The `error.data.code` a Host relays when the dialog a call came from is no longer waiting for an
|
|
81
|
+
* answer (answered elsewhere, expired, or its run is gone). Retrying cannot succeed, so a surface
|
|
82
|
+
* that sees it keeps its buttons disabled instead of re-arming them.
|
|
83
|
+
*/
|
|
84
|
+
export declare const SURFACE_NOT_PENDING_ERROR_CODE = "SURFACE_NOT_PENDING";
|
|
79
85
|
/** The id {@link renderStatusRegion} emits and {@link SURFACE_SCRIPT_PRELUDE} looks up. */
|
|
80
86
|
export declare const SURFACE_STATUS_ELEMENT_ID = "mcpui-status";
|
|
81
87
|
/**
|
|
@@ -96,6 +102,8 @@ export interface SurfaceStatusText {
|
|
|
96
102
|
readonly dismissed: string;
|
|
97
103
|
/** Prefixed to the comma-joined labels of unfilled required fields. */
|
|
98
104
|
readonly missingPrefix: string;
|
|
105
|
+
/** Shown when the Host says the dialog is no longer pending; the buttons stay disabled. */
|
|
106
|
+
readonly expired: string;
|
|
99
107
|
}
|
|
100
108
|
export declare const DEFAULT_SURFACE_STATUS_TEXT: SurfaceStatusText;
|
|
101
109
|
/**
|
|
@@ -150,6 +158,8 @@ export interface SurfaceAction {
|
|
|
150
158
|
* than that sandbox; every current builder passes `button` (the default).
|
|
151
159
|
*/
|
|
152
160
|
readonly type?: 'button' | 'submit';
|
|
161
|
+
/** Renders the button `disabled`; the surface script decides when to enable it. */
|
|
162
|
+
readonly disabled?: boolean;
|
|
153
163
|
}
|
|
154
164
|
/**
|
|
155
165
|
* Renders the action row.
|
|
@@ -128,7 +128,7 @@ body {
|
|
|
128
128
|
.mcpui-button-danger:hover:not([disabled]) { background: var(--jini-mcpui-danger-hover); border-color: var(--jini-mcpui-danger-hover); }
|
|
129
129
|
.mcpui-button[disabled] { opacity: 0.55; cursor: default; }
|
|
130
130
|
.mcpui-status { margin: 12px 0 0; min-height: 1.4em; color: var(--jini-mcpui-text-muted); }
|
|
131
|
-
.mcpui-status[data-state="failed"], .mcpui-status[data-state="invalid"] { color: var(--jini-mcpui-danger); }
|
|
131
|
+
.mcpui-status[data-state="failed"], .mcpui-status[data-state="invalid"], .mcpui-status[data-state="expired"] { color: var(--jini-mcpui-danger); }
|
|
132
132
|
.mcpui-status[data-state="done"] { color: var(--jini-mcpui-text-strong); }
|
|
133
133
|
/* Neither a plain success nor a failure — e.g. an outcome surface reporting "uploaded, not yet
|
|
134
134
|
reachable" (outcome.ts's own state: partial). Distinct from both --jini-mcpui-danger (would
|
|
@@ -217,6 +217,12 @@ export function renderDetailList(details) {
|
|
|
217
217
|
export function renderStatusRegion() {
|
|
218
218
|
return `<p class="mcpui-status" id="${SURFACE_STATUS_ELEMENT_ID}" role="status" aria-live="polite" data-state="idle"></p>`;
|
|
219
219
|
}
|
|
220
|
+
/**
|
|
221
|
+
* The `error.data.code` a Host relays when the dialog a call came from is no longer waiting for an
|
|
222
|
+
* answer (answered elsewhere, expired, or its run is gone). Retrying cannot succeed, so a surface
|
|
223
|
+
* that sees it keeps its buttons disabled instead of re-arming them.
|
|
224
|
+
*/
|
|
225
|
+
export const SURFACE_NOT_PENDING_ERROR_CODE = 'SURFACE_NOT_PENDING';
|
|
220
226
|
/** The id {@link renderStatusRegion} emits and {@link SURFACE_SCRIPT_PRELUDE} looks up. */
|
|
221
227
|
export const SURFACE_STATUS_ELEMENT_ID = 'mcpui-status';
|
|
222
228
|
export const DEFAULT_SURFACE_STATUS_TEXT = {
|
|
@@ -225,6 +231,7 @@ export const DEFAULT_SURFACE_STATUS_TEXT = {
|
|
|
225
231
|
failedPrefix: 'Failed: ',
|
|
226
232
|
dismissed: 'Dismissed.',
|
|
227
233
|
missingPrefix: 'Please complete: ',
|
|
234
|
+
expired: 'This dialog expired. Ask again.',
|
|
228
235
|
};
|
|
229
236
|
/**
|
|
230
237
|
* The JavaScript preamble every surface script opens with: the bridge handle, the status region,
|
|
@@ -248,6 +255,19 @@ function setBusy(busy) {
|
|
|
248
255
|
}
|
|
249
256
|
function describeError(error) {
|
|
250
257
|
return error && typeof error.message === "string" ? error.message : String(error);
|
|
258
|
+
}
|
|
259
|
+
// Reports a rejected tool call and returns whether the human may try again. A rejected call did not
|
|
260
|
+
// happen, so normally the buttons re-arm; but a dialog the Host no longer waits on cannot succeed,
|
|
261
|
+
// and live-looking buttons there only invite clicks that fail the same way. Reads TEXT, which each
|
|
262
|
+
// surface script declares after this prelude.
|
|
263
|
+
function reportCallFailure(error) {
|
|
264
|
+
if (error && error.data && error.data.code === ${JSON.stringify(SURFACE_NOT_PENDING_ERROR_CODE)}) {
|
|
265
|
+
setStatus(TEXT.expired, "expired");
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
setBusy(false);
|
|
269
|
+
setStatus(TEXT.failedPrefix + describeError(error), "failed");
|
|
270
|
+
return true;
|
|
251
271
|
}`;
|
|
252
272
|
/**
|
|
253
273
|
* A field name that is simultaneously an HTML `name`, part of a DOM `id`, and a key in the JSON
|
|
@@ -319,7 +339,7 @@ export function renderActions(actions) {
|
|
|
319
339
|
// would accept. That is unenforced on purpose (see the function doc above: `page.find_elements`
|
|
320
340
|
// can never reach this document to resolve it either way), so failing loudly here would refuse
|
|
321
341
|
// markup for a reason that can never matter to the one consumer that can actually query it.
|
|
322
|
-
return ` <button type="${action.type ?? 'button'}" class="mcpui-button${variantClass}" data-mcpui-action="${escapeHtml(action.id)}" ${AGENT_ELEMENT_ATTRIBUTE}="mcpui-action-${escapeHtml(action.id)}" ${AGENT_ROLE_ATTRIBUTE}="button" ${AGENT_LABEL_ATTRIBUTE}="${label}">${label}</button>`;
|
|
342
|
+
return ` <button type="${action.type ?? 'button'}" class="mcpui-button${variantClass}" data-mcpui-action="${escapeHtml(action.id)}" ${AGENT_ELEMENT_ATTRIBUTE}="mcpui-action-${escapeHtml(action.id)}" ${AGENT_ROLE_ATTRIBUTE}="button" ${AGENT_LABEL_ATTRIBUTE}="${label}"${action.disabled === true ? ' disabled' : ''}>${label}</button>`;
|
|
323
343
|
})
|
|
324
344
|
.join('\n');
|
|
325
345
|
return `<div class="mcpui-actions">\n${buttons}\n</div>`;
|
|
@@ -120,7 +120,8 @@ ${SURFACE_SCRIPT_PRELUDE}
|
|
|
120
120
|
//
|
|
121
121
|
// Deliberately NOT cleared on success -- the success path tears the surface down, so re-arming
|
|
122
122
|
// would only reopen the window. Cleared on failure, matching what setBusy(false) already does
|
|
123
|
-
// there: a rejected call did not happen, and the human must be able to retry
|
|
123
|
+
// there: a rejected call did not happen, and the human must be able to retry -- unless the Host
|
|
124
|
+
// says the dialog is no longer pending, where a retry cannot succeed (see reportCallFailure).
|
|
124
125
|
var pending = false;
|
|
125
126
|
|
|
126
127
|
function runSubmit() {
|
|
@@ -145,9 +146,7 @@ ${SURFACE_SCRIPT_PRELUDE}
|
|
|
145
146
|
setStatus(TEXT.done, "done");
|
|
146
147
|
api.requestTeardown();
|
|
147
148
|
}, function (error) {
|
|
148
|
-
pending = false;
|
|
149
|
-
setBusy(false);
|
|
150
|
-
setStatus(TEXT.failedPrefix + describeError(error), "failed");
|
|
149
|
+
if (reportCallFailure(error)) pending = false;
|
|
151
150
|
});
|
|
152
151
|
}
|
|
153
152
|
|
|
@@ -155,8 +154,10 @@ ${SURFACE_SCRIPT_PRELUDE}
|
|
|
155
154
|
// native behavior a single-line text input gets inside a real form. Scoped to the same controls
|
|
156
155
|
// the browser would have honored it for -- type=text/type=number inputs -- so Enter still inserts
|
|
157
156
|
// a newline in a multi-line textarea and still just toggles a checkbox.
|
|
157
|
+
// Only what the browser attributes to a person counts: element.click(), dispatchEvent and a
|
|
158
|
+
// synthetic keydown from any script produce isTrusted === false, and submit and cancel call tools.
|
|
158
159
|
form.addEventListener("keydown", function (event) {
|
|
159
|
-
if (event.key !== "Enter") return;
|
|
160
|
+
if (event.isTrusted !== true || event.key !== "Enter") return;
|
|
160
161
|
var target = event.target;
|
|
161
162
|
if (!target || target.tagName !== "INPUT") return;
|
|
162
163
|
var type = (target.getAttribute("type") || "text").toLowerCase();
|
|
@@ -168,12 +169,14 @@ ${SURFACE_SCRIPT_PRELUDE}
|
|
|
168
169
|
for (var b = 0; b < actionButtons.length; b++) {
|
|
169
170
|
var action = actionButtons[b].getAttribute("data-mcpui-action");
|
|
170
171
|
if (action === "submit") {
|
|
171
|
-
actionButtons[b].addEventListener("click",
|
|
172
|
+
actionButtons[b].addEventListener("click", function (event) {
|
|
173
|
+
if (event.isTrusted === true) runSubmit();
|
|
174
|
+
});
|
|
172
175
|
continue;
|
|
173
176
|
}
|
|
174
177
|
if (action !== "cancel") continue;
|
|
175
|
-
actionButtons[b].addEventListener("click", function () {
|
|
176
|
-
if (pending) return;
|
|
178
|
+
actionButtons[b].addEventListener("click", function (event) {
|
|
179
|
+
if (event.isTrusted !== true || pending) return;
|
|
177
180
|
pending = true;
|
|
178
181
|
setBusy(true);
|
|
179
182
|
if (CANCEL === null) {
|
|
@@ -186,9 +189,7 @@ ${SURFACE_SCRIPT_PRELUDE}
|
|
|
186
189
|
setStatus(TEXT.dismissed, "dismissed");
|
|
187
190
|
api.requestTeardown();
|
|
188
191
|
}, function (error) {
|
|
189
|
-
pending = false;
|
|
190
|
-
setBusy(false);
|
|
191
|
-
setStatus(TEXT.failedPrefix + describeError(error), "failed");
|
|
192
|
+
if (reportCallFailure(error)) pending = false;
|
|
192
193
|
});
|
|
193
194
|
});
|
|
194
195
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jini-ai/ui",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.11",
|
|
4
4
|
"description": "Generic, product-neutral React UI primitives and feature domains. Framework-free types, decision rules, catalogs, and host port contracts live behind ./core (no React, no DOM). The Excalidraw and Lexical editors live behind ./sketch-editor and ./lexical-rich-text-editor; the GrapesJS-backed inline HTML editor lives behind ./html-editor.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -102,13 +102,14 @@
|
|
|
102
102
|
"clsx": "^2.1.1",
|
|
103
103
|
"micromark": "^4.0.0",
|
|
104
104
|
"micromark-extension-gfm": "^3.0.0",
|
|
105
|
+
"parse5": "^8.0.1",
|
|
105
106
|
"recharts": "^3.10.1",
|
|
106
107
|
"shiki": "^1.24.4",
|
|
107
108
|
"tailwind-merge": "^2.6.0",
|
|
108
109
|
"zod": "^3.25.76",
|
|
109
|
-
"@jini-ai/
|
|
110
|
+
"@jini-ai/agentic": "0.3.6",
|
|
110
111
|
"@jini-ai/platform": "0.3.0",
|
|
111
|
-
"@jini-ai/
|
|
112
|
+
"@jini-ai/protocol": "0.3.0"
|
|
112
113
|
},
|
|
113
114
|
"devDependencies": {
|
|
114
115
|
"@excalidraw/excalidraw": "0.18.1",
|