feat: load plugin frontends at runtime and remove plugin ids from the shell

This commit is contained in:
LukeGus committed 2026-09-22 20:15:52 -05:00
1 parent 49f0286a14
commit e88d5a52e3
721 files changed
+54265 -44416

No files matched your search

+107 -1
View File
@@ -1 +1,107 @@
{}
{
"permissions": {
"use": {
"title": "Use the assistant",
"description": "Open the assistant and ask it things."
},
"manage_providers": {
"title": "Manage providers",
"description": "Add, edit and remove the AI providers and their API keys."
},
"apply_proposals": {
"title": "Apply proposals",
"description": "Let the assistant carry out the changes it suggests."
},
"services.use": {
"title": "Use AI from other plugins",
"description": "Other plugins may call the assistant on your behalf."
},
"secrets.share": {
"title": "Share the API key",
"description": "Other plugins may use the stored AI API key without seeing it."
}
},
"settings": {
"assistantUnavailable": "The AI assistant is turned off on this server."
},
"ai": {
"title": "Assistant",
"send": "Send",
"stop": "Stop",
"save": "Save",
"cancel": "Cancel",
"running": "running",
"approve": "Approve",
"reject": "Reject",
"inputPlaceholder": "Ask about your servers, or describe a change to propose",
"selectProvider": "Select a provider",
"providerSettings": "Provider settings",
"addProvider": "Add provider",
"editProvider": "Edit provider",
"removeProvider": "Remove provider",
"providerType": "Provider",
"providerLabel": "Name",
"providerLabelPlaceholder": "For example: Local Ollama",
"baseUrl": "Server address",
"apiKey": "API key",
"defaultModel": "Default model",
"defaultModelPlaceholder": "For example: llama3.1 or claude-opus-5",
"labelRequired": "Give this provider a name",
"providerSaveFailed": "Could not save the provider",
"providerUpdated": "Provider updated",
"providerDeleteFailed": "Could not remove the provider",
"privateEndpointHint": "Addresses on your private network need an administrator to allow the host first.",
"providerOllama": "Ollama",
"providerAnthropic": "Anthropic",
"providerOpenai": "OpenAI",
"providerGemini": "Google Gemini",
"providerOpenaiCompatible": "OpenAI compatible",
"toolArguments": "Input",
"toolResult": "Result",
"statusApplied": "Applied",
"statusRejected": "Rejected",
"proposalApplied": "Change applied",
"proposalApplyFailed": "Could not apply the change",
"proposalRejectFailed": "Could not reject the change",
"enableTitle": "Enable the AI assistant",
"enableDescription": "Let the assistant read your setup and suggest changes for you to approve.",
"readOnlyCommands": "Allow read-only diagnostic commands",
"readOnlyCommandsDescription": "The assistant may run a short list of safe commands like df and uptime without asking each time. Anything that changes a server is always proposed first.",
"modelDetectFailed": "Could not reach the provider, so this is a general list. You can type any model name.",
"modelCustom": "Enter a model name",
"modelRefresh": "Refresh list",
"modelPlaceholder": "Choose or type a model",
"attachHint": "Type @ to reference a host, snippet or automation. Changes are proposed for you to approve.",
"runCommandTitle": "Run command",
"assistant": "AI Assistant",
"terminalPanelEmpty": "Ask the assistant about this host. Anything that changes state needs your approval first.",
"terminalPanelPlaceholder": "Ask about this host...",
"noProvidersConfigured": "No AI provider is configured yet. Add one in the AI assistant tab.",
"runsInOpenTerminal": "Runs in your open terminal session.",
"runInTerminal": "Run",
"terminalContextPrefill": "Here is my terminal output:\n\n{{output}}\n\n",
"secrets": {
"shareApiKey": "Let other plugins use your AI provider API key for their own requests. The key is never copied, and you can revoke this at any time."
}
},
"nav": {
"ai": "AI"
},
"onboarding": {
"aiTitle": "AI assistant",
"aiIntro": "Termix can include an optional AI assistant. You can change this later in your settings.",
"aiSkipTitle": "Leave it off",
"aiSkipDesc": "Termix works as it does now. The assistant stays hidden.",
"aiEnableTitle": "Turn it on",
"aiEnableDesc": "Adds an assistant that reads your setup and suggests changes you approve. Needs a provider you supply, such as a local Ollama.",
"aiNote": "Nothing is sent anywhere until you add a provider and send a message."
},
"admin": {
"aiGloballyEnabled": "AI assistant",
"aiGloballyEnabledDesc": "Let users turn on the AI assistant. While this is off, the assistant is hidden and blocked for everyone.",
"aiPrivateEndpoints": "Allowed private AI hosts",
"aiPrivateEndpointsDesc": "Hosts on your private network that users may point a provider at, such as a self-hosted Ollama. Separate them with commas.",
"updateAiEnabledFailed": "Could not update the AI setting",
"updateAiEndpointsFailed": "Could not update the allowed hosts"
}
}
+78
View File
@@ -0,0 +1,78 @@
{
"ai": {
"title": "Assistent",
"send": "Stuur",
"stop": "Stop",
"save": "Stoor",
"cancel": "Kanselleer",
"running": "hardloop",
"approve": "Goedkeur",
"reject": "Verwerp",
"inputPlaceholder": "Vra oor jou bedieners, of beskryf 'n verandering om voor te stel",
"selectProvider": "Kies 'n verskaffer",
"providerSettings": "Verskafferinstellings",
"addProvider": "Voeg verskaffer by",
"editProvider": "Wysig verskaffer",
"removeProvider": "Verwyder verskaffer",
"providerType": "Verskaffer",
"providerLabel": "Naam",
"providerLabelPlaceholder": "Byvoorbeeld: Plaaslike Ollama",
"baseUrl": "Bedieneradres",
"apiKey": "API-sleutel",
"defaultModel": "Standaardmodel",
"defaultModelPlaceholder": "Byvoorbeeld: llama3.1 of claude-opus-5",
"labelRequired": "Gee hierdie verskaffer 'n naam",
"providerSaveFailed": "Kon nie die verskaffer stoor nie",
"providerUpdated": "Verskaffer opgedateer",
"providerDeleteFailed": "Kon nie die verskaffer verwyder nie",
"privateEndpointHint": "Adresse op jou private netwerk benodig eers 'n administrateur om die gasheer toe te laat.",
"providerOllama": "Ollama",
"providerAnthropic": "Antropiese",
"providerOpenai": "OpenAI",
"providerGemini": "Google Tweeling",
"providerOpenaiCompatible": "OpenAI-versoenbaar",
"toolArguments": "Invoer",
"toolResult": "Resultaat",
"statusApplied": "Toegepas",
"statusRejected": "Verwerp",
"proposalApplied": "Verandering toegepas",
"proposalApplyFailed": "Kon nie die verandering toepas nie",
"proposalRejectFailed": "Kon nie die verandering verwerp nie",
"enableTitle": "Aktiveer die KI-assistent",
"enableDescription": "Laat die assistent jou opstelling lees en veranderinge voorstel wat jy kan goedkeur.",
"readOnlyCommands": "Laat leesalleen-diagnostiese bevele toe",
"readOnlyCommandsDescription": "Die assistent kan 'n kort lys veilige opdragte soos df en uptime uitvoer sonder om elke keer te vra. Enigiets wat 'n bediener verander, word altyd eerste voorgestel.",
"modelDetectFailed": "Kon nie die verskaffer bereik nie, so dit is 'n algemene lys. Jy kan enige modelnaam tik.",
"modelCustom": "Voer 'n modelnaam in",
"modelRefresh": "Verfris lys",
"modelPlaceholder": "Kies of tik 'n model",
"attachHint": "Tik @ om na 'n gasheer, brokkie of outomatisering te verwys. Veranderinge word voorgestel vir jou om goed te keur.",
"runCommandTitle": "Voer opdrag uit",
"assistant": "KI-assistent",
"terminalPanelEmpty": "Vra die assistent oor hierdie gasheer. Enigiets wat van status verander, benodig eers jou goedkeuring.",
"terminalPanelPlaceholder": "Vra oor hierdie gasheer...",
"noProvidersConfigured": "Geen KI-verskaffer is nog gekonfigureer nie. Voeg een by in die KI-assistent-oortjie.",
"runsInOpenTerminal": "Loop in jou oop terminaalsessie.",
"runInTerminal": "Hardloop"
},
"nav": {
"ai": "KI"
},
"onboarding": {
"aiTitle": "KI-assistent",
"aiIntro": "Termix kan 'n opsionele KI-assistent insluit. Jy kan dit later in jou instellings verander.",
"aiSkipTitle": "Los dit af",
"aiSkipDesc": "Termix werk soos dit nou doen. Die assistent bly versteek.",
"aiEnableTitle": "Skakel dit aan",
"aiEnableDesc": "Voeg 'n assistent by wat jou opstelling lees en veranderinge voorstel wat jy goedkeur. Benodig 'n verskaffer wat jy verskaf, soos 'n plaaslike Ollama.",
"aiNote": "Niks word êrens heen gestuur totdat jy 'n verskaffer byvoeg en 'n boodskap stuur nie."
},
"admin": {
"aiGloballyEnabled": "KI-assistent",
"aiGloballyEnabledDesc": "Laat gebruikers die KI-assistent aanskakel. Terwyl dit af is, is die assistent vir almal versteek en geblokkeer.",
"aiPrivateEndpoints": "Toegelate private KI-gashere",
"aiPrivateEndpointsDesc": "Gashere op jou privaat netwerk waarna gebruikers 'n verskaffer kan verwys, soos 'n self-gehoste Ollama. Skei hulle met kommas.",
"updateAiEnabledFailed": "Kon nie die KI-instelling opdateer nie",
"updateAiEndpointsFailed": "Kon nie die toegelate gashere opdateer nie"
}
}
+78
View File
@@ -0,0 +1,78 @@
{
"ai": {
"title": "مساعد",
"send": "يرسل",
"stop": "قف",
"save": "يحفظ",
"cancel": "يلغي",
"running": "جري",
"approve": "يعتمد",
"reject": "يرفض",
"inputPlaceholder": "استفسر عن خوادمك، أو صف تغييرًا ترغب في اقتراحه.",
"selectProvider": "اختر مزود الخدمة",
"providerSettings": "إعدادات المزوّد",
"addProvider": "إضافة موفر",
"editProvider": "موفر التحرير",
"removeProvider": "إزالة الموفر",
"providerType": "مزود",
"providerLabel": "اسم",
"providerLabelPlaceholder": "على سبيل المثال: أولاما المحلي",
"baseUrl": "عنوان الخادم",
"apiKey": "مفتاح API",
"defaultModel": "النموذج الافتراضي",
"defaultModelPlaceholder": "على سبيل المثال: llama3.1 أو claude-opus-5",
"labelRequired": "أعطِ هذا المزوّد اسمًا",
"providerSaveFailed": "تعذر حفظ بيانات المزوّد",
"providerUpdated": "تم تحديث المزوّد",
"providerDeleteFailed": "تعذر إزالة الموفر",
"privateEndpointHint": "تحتاج العناوين الموجودة على شبكتك الخاصة إلى موافقة المسؤول أولاً للسماح للمضيف بالوصول إليها.",
"providerOllama": "أولاما",
"providerAnthropic": "الأنثروبيك",
"providerOpenai": "أوبن إيه آي",
"providerGemini": "جوجل جيميني",
"providerOpenaiCompatible": "متوافق مع OpenAI",
"toolArguments": "مدخل",
"toolResult": "نتيجة",
"statusApplied": "مُطبَّق",
"statusRejected": "مرفوض",
"proposalApplied": "تم تطبيق التغيير",
"proposalApplyFailed": "تعذر تطبيق التغيير",
"proposalRejectFailed": "لم يكن بالإمكان رفض التغيير",
"enableTitle": "قم بتفعيل مساعد الذكاء الاصطناعي",
"enableDescription": "دع المساعد يقرأ إعداداتك ويقترح تغييرات لتوافق عليها.",
"readOnlyCommands": "السماح بأوامر التشخيص للقراءة فقط",
"readOnlyCommandsDescription": "قد يقوم المساعد بتشغيل قائمة مختصرة من الأوامر الآمنة مثل df و uptime دون الحاجة إلى طلب ذلك في كل مرة. ويتم اقتراح أي أمر يُغيّر حالة الخادم أولاً.",
"modelDetectFailed": "تعذر الوصول إلى المورّد، لذا فهذه قائمة عامة. يمكنك كتابة أي اسم طراز.",
"modelCustom": "أدخل اسم النموذج",
"modelRefresh": "تحديث القائمة",
"modelPlaceholder": "اختر أو اكتب طرازًا",
"attachHint": "اكتب @ للإشارة إلى مضيف أو مقتطف أو عملية آلية. سيتم اقتراح تغييرات عليك الموافقة عليها.",
"runCommandTitle": "تشغيل الأمر",
"assistant": "مساعد الذكاء الاصطناعي",
"terminalPanelEmpty": "استفسر من المساعد عن هذا المضيف. أي تغيير في حالته يتطلب موافقتك أولاً.",
"terminalPanelPlaceholder": "اسأل عن هذا المضيف...",
"noProvidersConfigured": "لم يتم إعداد أي موفر للذكاء الاصطناعي حتى الآن. أضف واحداً في علامة تبويب مساعد الذكاء الاصطناعي.",
"runsInOpenTerminal": "يتم تشغيله في جلسة الطرفية المفتوحة لديك.",
"runInTerminal": "يجري"
},
"nav": {
"ai": "الذكاء الاصطناعي"
},
"onboarding": {
"aiTitle": "مساعد الذكاء الاصطناعي",
"aiIntro": "يمكن أن يتضمن تطبيق Termix مساعدًا ذكيًا اختياريًا. يمكنك تغيير هذا الخيار لاحقًا من الإعدادات.",
"aiSkipTitle": "اتركه مغلقًا",
"aiSkipDesc": "يعمل برنامج Termix كما هو الآن. ويبقى المساعد مخفياً.",
"aiEnableTitle": "قم بتشغيله",
"aiEnableDesc": "يُضيف هذا البرنامج مساعدًا يقرأ إعداداتك ويقترح تغييرات توافق عليها. يتطلب ذلك مزود خدمة تُحدده بنفسك، مثل مزود خدمة Ollama محلي.",
"aiNote": "لن يتم إرسال أي شيء إلى أي مكان حتى تقوم بإضافة مزود خدمة وإرسال رسالة."
},
"admin": {
"aiGloballyEnabled": "مساعد الذكاء الاصطناعي",
"aiGloballyEnabledDesc": "اسمح للمستخدمين بتفعيل مساعد الذكاء الاصطناعي. عند إيقاف تشغيله، يكون المساعد مخفيًا ومحظورًا عن الجميع.",
"aiPrivateEndpoints": "المضيفون الخاصون المسموح بهم للذكاء الاصطناعي",
"aiPrivateEndpointsDesc": "المضيفون على شبكتك الخاصة الذين يمكن للمستخدمين توجيه مزود الخدمة إليهم، مثل Ollama المستضاف ذاتيًا. افصل بينهم بفواصل.",
"updateAiEnabledFailed": "تعذر تحديث إعدادات الذكاء الاصطناعي",
"updateAiEndpointsFailed": "تعذر تحديث المضيفين المسموح بهم"
}
}
+78
View File
@@ -0,0 +1,78 @@
{
"ai": {
"title": "Асистент",
"send": "Изпрати",
"stop": "Спри",
"save": "Запазване",
"cancel": "Отказ",
"running": "бягане",
"approve": "Одобряване",
"reject": "Отхвърляне",
"inputPlaceholder": "Попитайте за вашите сървъри или опишете промяна, която да предложите",
"selectProvider": "Изберете доставчик",
"providerSettings": "Настройки на доставчика",
"addProvider": "Добавяне на доставчик",
"editProvider": "Редактиране на доставчика",
"removeProvider": "Премахване на доставчика",
"providerType": "Доставчик",
"providerLabel": "Име",
"providerLabelPlaceholder": "Например: Местна Олама",
"baseUrl": "Адрес на сървъра",
"apiKey": "API ключ",
"defaultModel": "Модел по подразбиране",
"defaultModelPlaceholder": "Например: llama3.1 или claude-opus-5",
"labelRequired": "Дайте име на този доставчик",
"providerSaveFailed": "Доставчикът не можа да бъде запазен",
"providerUpdated": "Доставчикът е актуализиран",
"providerDeleteFailed": "Доставчикът не можа да бъде премахнат",
"privateEndpointHint": "Адресите във вашата частна мрежа се нуждаят първо от администратор, който да разреши хоста.",
"providerOllama": "Олама",
"providerAnthropic": "Антропен",
"providerOpenai": "OpenAI",
"providerGemini": "Google Джемини",
"providerOpenaiCompatible": "Съвместим с OpenAI",
"toolArguments": "Вход",
"toolResult": "Резултат",
"statusApplied": "Приложено",
"statusRejected": "Отхвърлено",
"proposalApplied": "Промяната е приложена",
"proposalApplyFailed": "Промяната не можа да се приложи",
"proposalRejectFailed": "Промяната не можа да бъде отхвърлена",
"enableTitle": "Активирайте AI асистента",
"enableDescription": "Нека асистентът прочете настройката ви и предложи промени, които да одобрите.",
"readOnlyCommands": "Разрешаване на диагностични команди само за четене",
"readOnlyCommandsDescription": "Асистентът може да изпълни кратък списък с безопасни команди като df и uptime, без да пита всеки път. Всичко, което променя сървър, винаги се предлага първо.",
"modelDetectFailed": "Не можахме да се свържем с доставчика, така че това е общ списък. Можете да въведете име на произволен модел.",
"modelCustom": "Въведете име на модел",
"modelRefresh": "Обновяване на списъка",
"modelPlaceholder": "Изберете или въведете модел",
"attachHint": "Въведете @, за да се обърнете към хост, фрагмент или автоматизация. Предлагат се промени за одобрение.",
"runCommandTitle": "Изпълнете командата",
"assistant": "Асистент с изкуствен интелект",
"terminalPanelEmpty": "Попитайте асистента за този хост. Всичко, което променя състоянието, първо изисква вашето одобрение.",
"terminalPanelPlaceholder": "Попитайте за този домакин...",
"noProvidersConfigured": "Все още не е конфигуриран доставчик на ИИ. Добавете такъв в раздела „Асистент за ИИ“.",
"runsInOpenTerminal": "Изпълнява се във вашата отворена терминална сесия.",
"runInTerminal": "Бягане"
},
"nav": {
"ai": "Изкуствен интелект"
},
"onboarding": {
"aiTitle": "Асистент с изкуствен интелект",
"aiIntro": "Termix може да включва опционален AI асистент. Можете да промените това по-късно в настройките си.",
"aiSkipTitle": "Остави го изключено",
"aiSkipDesc": "Termix работи както сега. Асистентът остава скрит.",
"aiEnableTitle": "Включете го",
"aiEnableDesc": "Добавя асистент, който чете настройките ви и предлага промени, които одобрявате. Необходим е доставчик, когото вие предоставяте, например местен Ollama.",
"aiNote": "Нищо не се изпраща никъде, докато не добавите доставчик и не изпратите съобщение."
},
"admin": {
"aiGloballyEnabled": "Асистент с изкуствен интелект",
"aiGloballyEnabledDesc": "Позволете на потребителите да включат асистента с изкуствен интелект. Докато това е изключено, асистентът е скрит и блокиран за всички.",
"aiPrivateEndpoints": "Разрешени частни хостове с изкуствен интелект",
"aiPrivateEndpointsDesc": "Хостове във вашата частна мрежа, към които потребителите могат да насочат доставчик, като например самостоятелно хостван Ollama. Разделете ги със запетаи.",
"updateAiEnabledFailed": "Не можа да се актуализира настройката за изкуствен интелект",
"updateAiEndpointsFailed": "Не можаха да се актуализират разрешените хостове"
}
}
+78
View File
@@ -0,0 +1,78 @@
{
"ai": {
"title": "সহকারী",
"send": "পাঠান",
"stop": "থামুন",
"save": "সংরক্ষণ করুন",
"cancel": "বাতিল করুন",
"running": "দৌড়ানো",
"approve": "অনুমোদন করুন",
"reject": "প্রত্যাখ্যান করুন",
"inputPlaceholder": "আপনার সার্ভারগুলো সম্পর্কে জিজ্ঞাসা করুন, অথবা কোনো পরিবর্তনের প্রস্তাব বর্ণনা করুন।",
"selectProvider": "একজন সরবরাহকারী নির্বাচন করুন",
"providerSettings": "প্রদানকারীর সেটিংস",
"addProvider": "প্রদানকারী যোগ করুন",
"editProvider": "সম্পাদনা প্রদানকারী",
"removeProvider": "প্রদানকারীকে অপসারণ করুন",
"providerType": "সরবরাহকারী",
"providerLabel": "নাম",
"providerLabelPlaceholder": "উদাহরণস্বরূপ: স্থানীয় ওলামা",
"baseUrl": "সার্ভারের ঠিকানা",
"apiKey": "এপিআই কী",
"defaultModel": "ডিফল্ট মডেল",
"defaultModelPlaceholder": "উদাহরণস্বরূপ: llama3.1 অথবা claude-opus-5",
"labelRequired": "এই সরবরাহকারীকে একটি নাম দিন",
"providerSaveFailed": "প্রদানকারীকে সংরক্ষণ করা যায়নি",
"providerUpdated": "প্রদানকারী আপডেট করেছেন",
"providerDeleteFailed": "প্রদানকারীকে সরানো যায়নি",
"privateEndpointHint": "আপনার ব্যক্তিগত নেটওয়ার্কের অ্যাড্রেসগুলো ব্যবহার করার জন্য প্রথমে একজন অ্যাডমিনিস্ট্রেটরের কাছ থেকে হোস্টটিকে অনুমতি নিতে হয়।",
"providerOllama": "ওল্লামা",
"providerAnthropic": "অ্যানথ্রোপিক",
"providerOpenai": "ওপেনএআই",
"providerGemini": "গুগল জেমিনি",
"providerOpenaiCompatible": "ওপেনএআই সামঞ্জস্যপূর্ণ",
"toolArguments": "ইনপুট",
"toolResult": "ফলাফল",
"statusApplied": "প্রয়োগ করা হয়েছে",
"statusRejected": "প্রত্যাখ্যাত",
"proposalApplied": "পরিবর্তন প্রয়োগ করা হয়েছে",
"proposalApplyFailed": "পরিবর্তনটি প্রয়োগ করা যায়নি।",
"proposalRejectFailed": "পরিবর্তনটি প্রত্যাখ্যান করা যায়নি",
"enableTitle": "এআই অ্যাসিস্ট্যান্ট সক্রিয় করুন",
"enableDescription": "সহকারীকে আপনার সেটআপটি পড়তে দিন এবং আপনার অনুমোদনের জন্য পরিবর্তনের পরামর্শ দিতে দিন।",
"readOnlyCommands": "শুধুমাত্র পঠনযোগ্য ডায়াগনস্টিক কমান্ডের অনুমতি দিন",
"readOnlyCommandsDescription": "সহকারী প্রতিবার অনুমতি না নিয়েই df এবং uptime-এর মতো কয়েকটি নিরাপদ কমান্ড চালাতে পারে। সার্ভার পরিবর্তন করে এমন যেকোনো কিছু সর্বদা প্রথমে প্রস্তাব করা হয়।",
"modelDetectFailed": "সরবরাহকারীর সাথে যোগাযোগ করা সম্ভব হয়নি, তাই এটি একটি সাধারণ তালিকা। আপনি যেকোনো মডেলের নাম টাইপ করতে পারেন।",
"modelCustom": "একটি মডেলের নাম লিখুন",
"modelRefresh": "তালিকা রিফ্রেশ করুন",
"modelPlaceholder": "একটি মডেল বেছে নিন বা টাইপ করুন",
"attachHint": "কোনো হোস্ট, স্নিপেট বা অটোমেশন উল্লেখ করতে @ টাইপ করুন। আপনার অনুমোদনের জন্য পরিবর্তনগুলো প্রস্তাব করা হবে।",
"runCommandTitle": "রান কমান্ড",
"assistant": "এআই সহকারী",
"terminalPanelEmpty": "এই হোস্ট সম্পর্কে অ্যাসিস্ট্যান্টকে জিজ্ঞাসা করুন। অবস্থা পরিবর্তনের জন্য প্রথমে আপনার অনুমোদন প্রয়োজন।",
"terminalPanelPlaceholder": "এই হোস্ট সম্পর্কে জিজ্ঞাসা করুন...",
"noProvidersConfigured": "এখনো কোনো এআই প্রোভাইডার কনফিগার করা হয়নি। এআই অ্যাসিস্ট্যান্ট ট্যাবে একটি যোগ করুন।",
"runsInOpenTerminal": "আপনার খোলা টার্মিনাল সেশনে চলে।",
"runInTerminal": "দৌড়"
},
"nav": {
"ai": "এআই"
},
"onboarding": {
"aiTitle": "এআই সহকারী",
"aiIntro": "টারমিক্সে একটি ঐচ্ছিক এআই অ্যাসিস্ট্যান্ট অন্তর্ভুক্ত থাকতে পারে। আপনি পরবর্তীতে আপনার সেটিংসে এটি পরিবর্তন করতে পারবেন।",
"aiSkipTitle": "এটা ছেড়ে দাও",
"aiSkipDesc": "টারমিক্স এখন যেমন কাজ করে, তেমনই কাজ করে। অ্যাসিস্ট্যান্টটি লুকানো থাকে।",
"aiEnableTitle": "এটা চালু করুন",
"aiEnableDesc": "একজন সহকারী যোগ করে যে আপনার সেটআপ পড়ে এবং আপনার অনুমোদিত পরিবর্তনের পরামর্শ দেয়। এর জন্য আপনার সরবরাহ করা একজন প্রদানকারীর প্রয়োজন, যেমন একজন স্থানীয় ওলামা।",
"aiNote": "আপনি একজন প্রোভাইডার যোগ করে মেসেজ না পাঠানো পর্যন্ত কোথাও কিছু পাঠানো হয় না।"
},
"admin": {
"aiGloballyEnabled": "এআই সহকারী",
"aiGloballyEnabledDesc": "ব্যবহারকারীদের এআই অ্যাসিস্ট্যান্ট চালু করার সুযোগ দিন। এটি বন্ধ থাকা অবস্থায়, অ্যাসিস্ট্যান্টটি সকলের জন্য লুকানো এবং ব্লক করা থাকবে।",
"aiPrivateEndpoints": "ব্যক্তিগত এআই হোস্ট অনুমোদিত",
"aiPrivateEndpointsDesc": "আপনার প্রাইভেট নেটওয়ার্কের হোস্টগুলো, যেগুলোকে ব্যবহারকারীরা কোনো প্রোভাইডারের দিকে নির্দেশ করতে পারে, যেমন একটি সেলফ-হোস্টেড ওলামা। এগুলোকে কমা দিয়ে আলাদা করুন।",
"updateAiEnabledFailed": "এআই সেটিং আপডেট করা যায়নি",
"updateAiEndpointsFailed": "অনুমোদিত হোস্টগুলি আপডেট করা যায়নি"
}
}
+78
View File
@@ -0,0 +1,78 @@
{
"ai": {
"title": "Assistent",
"send": "Enviar",
"stop": "Atura",
"save": "Desa",
"cancel": "Cancel·la",
"running": "corrent",
"approve": "Aprova",
"reject": "Rebutja",
"inputPlaceholder": "Pregunteu pels vostres cambrers o descriviu un canvi que vulgueu proposar",
"selectProvider": "Seleccioneu un proveïdor",
"providerSettings": "Configuració del proveïdor",
"addProvider": "Afegeix un proveïdor",
"editProvider": "Edita el proveïdor",
"removeProvider": "Elimina el proveïdor",
"providerType": "Proveïdor",
"providerLabel": "Nom",
"providerLabelPlaceholder": "Per exemple: Ollama local",
"baseUrl": "Adreça del servidor",
"apiKey": "Clau API",
"defaultModel": "Model per defecte",
"defaultModelPlaceholder": "Per exemple: llama3.1 o claude-opus-5",
"labelRequired": "Doneu un nom a aquest proveïdor",
"providerSaveFailed": "No s'ha pogut desar el proveïdor",
"providerUpdated": "Proveïdor actualitzat",
"providerDeleteFailed": "No s'ha pogut eliminar el proveïdor",
"privateEndpointHint": "Les adreces de la vostra xarxa privada necessiten primer que un administrador permeti l'accés a l'amfitrió.",
"providerOllama": "Ollama",
"providerAnthropic": "Antròpic",
"providerOpenai": "OpenAI",
"providerGemini": "Google Bessons",
"providerOpenaiCompatible": "Compatible amb OpenAI",
"toolArguments": "Entrada",
"toolResult": "Resultat",
"statusApplied": "Aplicat",
"statusRejected": "Rebutjat",
"proposalApplied": "Canvi aplicat",
"proposalApplyFailed": "No s'ha pogut aplicar el canvi",
"proposalRejectFailed": "No s'ha pogut rebutjar el canvi",
"enableTitle": "Activa l'assistent d'IA",
"enableDescription": "Deixa que l'assistent llegeixi la teva configuració i et suggereixi canvis perquè els aprovis.",
"readOnlyCommands": "Permetre ordres de diagnòstic de només lectura",
"readOnlyCommandsDescription": "L'assistent pot executar una llista curta d'ordres segures com ara df i uptime sense preguntar cada vegada. Qualsevol cosa que canviï un servidor sempre es proposa primer.",
"modelDetectFailed": "No s'ha pogut contactar amb el proveïdor, per tant, aquesta és una llista general. Podeu escriure qualsevol nom de model.",
"modelCustom": "Introduïu un nom de model",
"modelRefresh": "Actualitza la llista",
"modelPlaceholder": "Trieu o escriviu un model",
"attachHint": "Escriviu @ per fer referència a un host, fragment o automatització. Es proposen canvis perquè els aprovi.",
"runCommandTitle": "Executa l'ordre",
"assistant": "Assistent d'IA",
"terminalPanelEmpty": "Pregunta a l'assistent sobre aquest amfitrió. Qualsevol cosa que canviï d'estat necessita primer la teva aprovació.",
"terminalPanelPlaceholder": "Pregunta per aquest amfitrió...",
"noProvidersConfigured": "Encara no hi ha cap proveïdor d'IA configurat. Afegeix-ne un a la pestanya Assistent d'IA.",
"runsInOpenTerminal": "S'executa a la sessió de terminal oberta.",
"runInTerminal": "Corre"
},
"nav": {
"ai": "IA"
},
"onboarding": {
"aiTitle": "Assistent d'IA",
"aiIntro": "Termix pot incloure un assistent d'IA opcional. Pots canviar-ho més tard a la configuració.",
"aiSkipTitle": "Deixa-ho fora",
"aiSkipDesc": "Termix funciona com ho fa ara. L'assistent roman ocult.",
"aiEnableTitle": "Engega-ho",
"aiEnableDesc": "Afegeix un assistent que llegeix la configuració i suggereix els canvis que aproveu. Necessita un proveïdor que subministreu, com ara un Ollama local.",
"aiNote": "No s'envia res enlloc fins que no afegiu un proveïdor i envieu un missatge."
},
"admin": {
"aiGloballyEnabled": "Assistent d'IA",
"aiGloballyEnabledDesc": "Permet que els usuaris activin l'assistent d'IA. Mentre estigui desactivat, l'assistent està ocult i bloquejat per a tothom.",
"aiPrivateEndpoints": "Amfitrions d'IA privats permesos",
"aiPrivateEndpointsDesc": "Amfitrions de la vostra xarxa privada als quals els usuaris poden dirigir un proveïdor, com ara un Ollama autoallotjat. Separeu-los amb comes.",
"updateAiEnabledFailed": "No s'ha pogut actualitzar la configuració de la IA",
"updateAiEndpointsFailed": "No s'han pogut actualitzar els amfitrions permesos"
}
}
+78
View File
@@ -0,0 +1,78 @@
{
"ai": {
"title": "Asistent",
"send": "Poslat",
"stop": "Zastávka",
"save": "Uložit",
"cancel": "Zrušit",
"running": "běh",
"approve": "Schvalovat",
"reject": "Odmítnout",
"inputPlaceholder": "Zeptejte se na své servery nebo popište změnu, kterou chcete navrhnout",
"selectProvider": "Vyberte poskytovatele",
"providerSettings": "Nastavení poskytovatele",
"addProvider": "Přidat poskytovatele",
"editProvider": "Upravit poskytovatele",
"removeProvider": "Odebrat poskytovatele",
"providerType": "Poskytovatel",
"providerLabel": "Jméno",
"providerLabelPlaceholder": "Například: Místní Ollama",
"baseUrl": "Adresa serveru",
"apiKey": "Klíč API",
"defaultModel": "Výchozí model",
"defaultModelPlaceholder": "Například: llama3.1 nebo claude-opus-5",
"labelRequired": "Zadejte tomuto poskytovateli název",
"providerSaveFailed": "Nepodařilo se uložit poskytovatele",
"providerUpdated": "Poskytovatel aktualizován",
"providerDeleteFailed": "Nepodařilo se odebrat poskytovatele",
"privateEndpointHint": "Adresy ve vaší privátní síti vyžadují, aby je správce nejprve povolil hostiteli.",
"providerOllama": "Ollama",
"providerAnthropic": "Antropický",
"providerOpenai": "OpenAI",
"providerGemini": "Google Gemini",
"providerOpenaiCompatible": "Kompatibilní s OpenAI",
"toolArguments": "Vstup",
"toolResult": "Výsledek",
"statusApplied": "Aplikovaný",
"statusRejected": "Zamítnuto",
"proposalApplied": "Změna použita",
"proposalApplyFailed": "Změnu se nepodařilo použít",
"proposalRejectFailed": "Změnu se nepodařilo odmítnout",
"enableTitle": "Povolte asistenta s umělou inteligencí",
"enableDescription": "Nechte asistenta přečíst si vaše nastavení a navrhnout změny ke schválení.",
"readOnlyCommands": "Povolit diagnostické příkazy pouze pro čtení",
"readOnlyCommandsDescription": "Asistent může spustit krátký seznam bezpečných příkazů, jako například df a uptime, aniž by se pokaždé ptal. Vše, co mění server, je vždy navrženo jako první.",
"modelDetectFailed": "Nepodařilo se mi spojit s poskytovatelem, toto je obecný seznam. Můžete zadat libovolný název modelu.",
"modelCustom": "Zadejte název modelu",
"modelRefresh": "Obnovit seznam",
"modelPlaceholder": "Vyberte nebo zadejte model",
"attachHint": "Zadejte @ pro odkaz na hostitele, úryvek kódu nebo automatizaci. Změny jsou navrženy ke schválení.",
"runCommandTitle": "Spustit příkaz",
"assistant": "Asistent umělé inteligence",
"terminalPanelEmpty": "Zeptejte se asistenta na tohoto hostitele. Jakákoli změna stavu vyžaduje váš první souhlas.",
"terminalPanelPlaceholder": "Zeptejte se na tohoto hostitele...",
"noProvidersConfigured": "Zatím není nakonfigurován žádný poskytovatel AI. Přidejte ho na kartě Asistent AI.",
"runsInOpenTerminal": "Spustí se ve vaší otevřené terminálové relaci.",
"runInTerminal": "Běh"
},
"nav": {
"ai": "Umělá inteligence"
},
"onboarding": {
"aiTitle": "Asistent s umělou inteligencí",
"aiIntro": "Termix může obsahovat volitelného asistenta s umělou inteligencí. Toto můžete později změnit v nastavení.",
"aiSkipTitle": "Nech to být",
"aiSkipDesc": "Termix funguje stejně jako teď. Asistent zůstává skrytý.",
"aiEnableTitle": "Zapněte to",
"aiEnableDesc": "Přidá asistenta, který si přečte vaše nastavení a navrhne změny, které schválíte. Vyžaduje poskytovatele, kterého dodáte, například místní Ollamu.",
"aiNote": "Nic se nikam neodesílá, dokud nepřidáte poskytovatele a neodešlete zprávu."
},
"admin": {
"aiGloballyEnabled": "Asistent s umělou inteligencí",
"aiGloballyEnabledDesc": "Umožněte uživatelům zapnout asistenta s umělou inteligencí. Pokud je tato funkce vypnutá, asistent je skrytý a blokovaný pro všechny.",
"aiPrivateEndpoints": "Povolení soukromí hostitelé s umělou inteligencí",
"aiPrivateEndpointsDesc": "Hostitelé ve vaší privátní síti, na které mohou uživatelé odkazovat poskytovatele, například samostatně hostovaný Ollama. Oddělte je čárkami.",
"updateAiEnabledFailed": "Nepodařilo se aktualizovat nastavení umělé inteligence",
"updateAiEndpointsFailed": "Nepodařilo se aktualizovat povolené hostitele"
}
}
+78
View File
@@ -0,0 +1,78 @@
{
"ai": {
"title": "Assistent",
"send": "Sende",
"stop": "Stop",
"save": "Spare",
"cancel": "Ophæve",
"running": "løb",
"approve": "Godkende",
"reject": "Afvise",
"inputPlaceholder": "Spørg om dine servere, eller beskriv en ændring, du vil foreslå",
"selectProvider": "Vælg en udbyder",
"providerSettings": "Udbyderindstillinger",
"addProvider": "Tilføj udbyder",
"editProvider": "Rediger udbyder",
"removeProvider": "Fjern udbyder",
"providerType": "Udbyder",
"providerLabel": "Navn",
"providerLabelPlaceholder": "For eksempel: Lokal Ollama",
"baseUrl": "Serveradresse",
"apiKey": "API-nøgle",
"defaultModel": "Standardmodel",
"defaultModelPlaceholder": "For eksempel: llama3.1 eller claude-opus-5",
"labelRequired": "Giv denne udbyder et navn",
"providerSaveFailed": "Udbyderen kunne ikke gemmes",
"providerUpdated": "Udbyder opdateret",
"providerDeleteFailed": "Udbyderen kunne ikke fjernes",
"privateEndpointHint": "Adresser på dit private netværk kræver først tilladelse fra en administrator til værten.",
"providerOllama": "Ollama",
"providerAnthropic": "Antropisk",
"providerOpenai": "OpenAI",
"providerGemini": "Google Gemini",
"providerOpenaiCompatible": "OpenAI-kompatibel",
"toolArguments": "Input",
"toolResult": "Resultat",
"statusApplied": "Anvendt",
"statusRejected": "Afvist",
"proposalApplied": "Ændring anvendt",
"proposalApplyFailed": "Ændringen kunne ikke anvendes",
"proposalRejectFailed": "Kunne ikke afvise ændringen",
"enableTitle": "Aktivér AI-assistenten",
"enableDescription": "Lad assistenten læse din opsætning og foreslå ændringer, som du kan godkende.",
"readOnlyCommands": "Tillad skrivebeskyttede diagnosticeringskommandoer",
"readOnlyCommandsDescription": "Assistenten kan køre en kort liste med sikre kommandoer som f.eks. df og uptime uden at spørge hver gang. Alt, der ændrer en server, foreslås altid først.",
"modelDetectFailed": "Kunne ikke få kontakt med udbyderen, så dette er en generel liste. Du kan skrive et hvilket som helst modelnavn.",
"modelCustom": "Indtast et modelnavn",
"modelRefresh": "Opdater listen",
"modelPlaceholder": "Vælg eller skriv en model",
"attachHint": "Skriv @ for at referere til en vært, et kodestykke eller en automatisering. Ændringer foreslås, som du skal godkende.",
"runCommandTitle": "Kør kommando",
"assistant": "AI-assistent",
"terminalPanelEmpty": "Spørg assistenten om denne vært. Alt, der ændrer status, kræver din godkendelse først.",
"terminalPanelPlaceholder": "Spørg om denne vært...",
"noProvidersConfigured": "Der er endnu ikke konfigureret nogen AI-udbyder. Tilføj en i fanen AI-assistent.",
"runsInOpenTerminal": "Kører i din åbne terminalsession.",
"runInTerminal": "Løbe"
},
"nav": {
"ai": "AI"
},
"onboarding": {
"aiTitle": "AI-assistent",
"aiIntro": "Termix kan inkludere en valgfri AI-assistent. Du kan ændre dette senere i dine indstillinger.",
"aiSkipTitle": "Lad det være slukket",
"aiSkipDesc": "Termix fungerer som den gør nu. Assistenten forbliver skjult.",
"aiEnableTitle": "Tænd den",
"aiEnableDesc": "Tilføjer en assistent, der læser din opsætning og foreslår ændringer, du godkender. Kræver en udbyder, du angiver, f.eks. en lokal Ollama.",
"aiNote": "Intet sendes nogen steder, før du tilføjer en udbyder og sender en besked."
},
"admin": {
"aiGloballyEnabled": "AI-assistent",
"aiGloballyEnabledDesc": "Lad brugerne aktivere AI-assistenten. Når dette er slået fra, er assistenten skjult og blokeret for alle.",
"aiPrivateEndpoints": "Tilladte private AI-værter",
"aiPrivateEndpointsDesc": "Værter på dit private netværk, som brugerne kan henvise til en udbyder, f.eks. en selvhostet Ollama. Adskil dem med kommaer.",
"updateAiEnabledFailed": "Kunne ikke opdatere AI-indstillingen",
"updateAiEndpointsFailed": "Kunne ikke opdatere de tilladte værter"
}
}
+78
View File
@@ -0,0 +1,78 @@
{
"ai": {
"title": "Assistent",
"send": "Schicken",
"stop": "Stoppen",
"save": "Speichern",
"cancel": "Stornieren",
"running": "läuft",
"approve": "Genehmigen",
"reject": "Ablehnen",
"inputPlaceholder": "Erkundigen Sie sich nach Ihren Servern oder beschreiben Sie einen Änderungsvorschlag.",
"selectProvider": "Wählen Sie einen Anbieter aus",
"providerSettings": "Anbietereinstellungen",
"addProvider": "Anbieter hinzufügen",
"editProvider": "Anbieter bearbeiten",
"removeProvider": "Anbieter entfernen",
"providerType": "Anbieter",
"providerLabel": "Name",
"providerLabelPlaceholder": "Zum Beispiel: Lokaler Ollama",
"baseUrl": "Serveradresse",
"apiKey": "API-Schlüssel",
"defaultModel": "Standardmodell",
"defaultModelPlaceholder": "Zum Beispiel: llama3.1 oder claude-opus-5",
"labelRequired": "Geben Sie diesem Anbieter einen Namen",
"providerSaveFailed": "Der Anbieter konnte nicht gespeichert werden.",
"providerUpdated": "Anbieter aktualisiert",
"providerDeleteFailed": "Der Anbieter konnte nicht entfernt werden.",
"privateEndpointHint": "Adressen in Ihrem privaten Netzwerk benötigen eine vorherige Genehmigung durch einen Administrator.",
"providerOllama": "Ollama",
"providerAnthropic": "Anthropisch",
"providerOpenai": "OpenAI",
"providerGemini": "Google Gemini",
"providerOpenaiCompatible": "OpenAI-kompatibel",
"toolArguments": "Eingang",
"toolResult": "Ergebnis",
"statusApplied": "Angewandt",
"statusRejected": "Abgelehnt",
"proposalApplied": "Änderung angewendet",
"proposalApplyFailed": "Die Änderung konnte nicht angewendet werden.",
"proposalRejectFailed": "Die Änderung konnte nicht abgelehnt werden.",
"enableTitle": "Aktivieren Sie den KI-Assistenten",
"enableDescription": "Lassen Sie den Assistenten Ihre Einstellungen lesen und Ihnen Änderungen zur Genehmigung vorschlagen.",
"readOnlyCommands": "Zulassen von schreibgeschützten Diagnosebefehlen",
"readOnlyCommandsDescription": "Der Assistent kann eine kurze Liste sicherer Befehle wie df und uptime ausführen, ohne jedes Mal nachzufragen. Alle Änderungen, die einen Server betreffen, werden immer zuerst vorgeschlagen.",
"modelDetectFailed": "Der Anbieter konnte nicht erreicht werden, daher handelt es sich um eine allgemeine Liste. Sie können einen beliebigen Modellnamen eingeben.",
"modelCustom": "Geben Sie einen Modellnamen ein",
"modelRefresh": "Liste aktualisieren",
"modelPlaceholder": "Wählen Sie ein Modell aus oder geben Sie es ein.",
"attachHint": "Geben Sie @ ein, um auf einen Host, ein Code-Snippet oder eine Automatisierung zu verweisen. Die Änderungen werden Ihnen zur Genehmigung vorgeschlagen.",
"runCommandTitle": "Befehl ausführen",
"assistant": "KI-Assistent",
"terminalPanelEmpty": "Fragen Sie den Assistenten nach diesem Gastgeber. Jede Statusänderung bedarf Ihrer vorherigen Zustimmung.",
"terminalPanelPlaceholder": "Erkundigen Sie sich nach diesem Gastgeber...",
"noProvidersConfigured": "Es ist noch kein KI-Anbieter konfiguriert. Fügen Sie einen im Tab „KI-Assistent“ hinzu.",
"runsInOpenTerminal": "Wird in Ihrer geöffneten Terminal-Sitzung ausgeführt.",
"runInTerminal": "Laufen"
},
"nav": {
"ai": "KI"
},
"onboarding": {
"aiTitle": "KI-Assistent",
"aiIntro": "Termix kann optional einen KI-Assistenten beinhalten. Sie können dies später in Ihren Einstellungen ändern.",
"aiSkipTitle": "Lass es weg",
"aiSkipDesc": "Termix funktioniert wie bisher. Der Assistent bleibt verborgen.",
"aiEnableTitle": "Schalte es ein",
"aiEnableDesc": "Fügt einen Assistenten hinzu, der Ihre Einstellungen analysiert und Ihnen von Ihnen genehmigte Änderungen vorschlägt. Benötigt einen von Ihnen angegebenen Anbieter, z. B. einen lokalen Ollama-Händler.",
"aiNote": "Es wird nichts gesendet, bis Sie einen Anbieter hinzufügen und eine Nachricht senden."
},
"admin": {
"aiGloballyEnabled": "KI-Assistent",
"aiGloballyEnabledDesc": "Ermöglichen Sie den Nutzern, den KI-Assistenten zu aktivieren. Solange diese Option deaktiviert ist, ist der Assistent für alle ausgeblendet und gesperrt.",
"aiPrivateEndpoints": "Zulässige private KI-Hosts",
"aiPrivateEndpointsDesc": "Hosts in Ihrem privaten Netzwerk, auf die Benutzer einen Provider verweisen können, wie z. B. ein selbst gehostetes Ollama. Trennen Sie diese durch Kommas.",
"updateAiEnabledFailed": "Die KI-Einstellungen konnten nicht aktualisiert werden.",
"updateAiEndpointsFailed": "Die Liste der zulässigen Hosts konnte nicht aktualisiert werden."
}
}
+78
View File
@@ -0,0 +1,78 @@
{
"ai": {
"title": "Βοηθός",
"send": "Στέλνω",
"stop": "Στάση",
"save": "Εκτός",
"cancel": "Ματαίωση",
"running": "τρέξιμο",
"approve": "Εγκρίνω",
"reject": "Απορρίπτω",
"inputPlaceholder": "Ρωτήστε για τους διακομιστές σας ή περιγράψτε μια αλλαγή που προτείνετε",
"selectProvider": "Επιλέξτε έναν πάροχο",
"providerSettings": "Ρυθμίσεις παρόχου",
"addProvider": "Προσθήκη παρόχου",
"editProvider": "Επεξεργασία παρόχου",
"removeProvider": "Κατάργηση παρόχου",
"providerType": "Προμηθευτής",
"providerLabel": "Ονομα",
"providerLabelPlaceholder": "Για παράδειγμα: Τοπικό Ollama",
"baseUrl": "Διεύθυνση διακομιστή",
"apiKey": "Κλειδί API",
"defaultModel": "Προεπιλεγμένο μοντέλο",
"defaultModelPlaceholder": "Για παράδειγμα: llama3.1 ή claude-opus-5",
"labelRequired": "Δώστε σε αυτόν τον πάροχο ένα όνομα",
"providerSaveFailed": "Δεν ήταν δυνατή η αποθήκευση του παρόχου",
"providerUpdated": "Ο πάροχος ενημερώθηκε",
"providerDeleteFailed": "Δεν ήταν δυνατή η κατάργηση του παρόχου",
"privateEndpointHint": "Οι διευθύνσεις στο ιδιωτικό σας δίκτυο χρειάζονται πρώτα έναν διαχειριστή για να επιτρέψει στον κεντρικό υπολογιστή.",
"providerOllama": "Ολάμα",
"providerAnthropic": "Ανθρώπινο",
"providerOpenai": "Ανοιχτή Τεχνητή Νοημοσύνη",
"providerGemini": "Google Gemini",
"providerOpenaiCompatible": "Συμβατό με OpenAI",
"toolArguments": "Εισαγωγή",
"toolResult": "Αποτέλεσμα",
"statusApplied": "Εφαρμοσμένος",
"statusRejected": "Απορρίφθηκε",
"proposalApplied": "Η αλλαγή εφαρμόστηκε",
"proposalApplyFailed": "Δεν ήταν δυνατή η εφαρμογή της αλλαγής",
"proposalRejectFailed": "Δεν ήταν δυνατή η απόρριψη της αλλαγής",
"enableTitle": "Ενεργοποίηση του βοηθού τεχνητής νοημοσύνης",
"enableDescription": "Αφήστε τον βοηθό να διαβάσει τη ρύθμισή σας και να σας προτείνει αλλαγές για έγκριση.",
"readOnlyCommands": "Να επιτρέπονται διαγνωστικές εντολές μόνο για ανάγνωση",
"readOnlyCommandsDescription": "Ο βοηθός μπορεί να εκτελέσει μια σύντομη λίστα ασφαλών εντολών όπως df και uptime χωρίς να ρωτάει κάθε φορά. Οτιδήποτε αλλάζει έναν διακομιστή προτείνεται πάντα πρώτο.",
"modelDetectFailed": "Δεν ήταν δυνατή η επικοινωνία με τον πάροχο, επομένως αυτή είναι μια γενική λίστα. Μπορείτε να πληκτρολογήσετε οποιοδήποτε όνομα μοντέλου.",
"modelCustom": "Εισαγάγετε ένα όνομα μοντέλου",
"modelRefresh": "Ανανέωση λίστας",
"modelPlaceholder": "Επιλέξτε ή πληκτρολογήστε ένα μοντέλο",
"attachHint": "Πληκτρολογήστε @ για να αναφέρετε έναν κεντρικό υπολογιστή, ένα απόσπασμα κώδικα ή έναν αυτοματισμό. Προτείνονται αλλαγές για έγκριση.",
"runCommandTitle": "Εκτέλεση εντολής",
"assistant": "Βοηθός Τεχνητής Νοημοσύνης",
"terminalPanelEmpty": "Ρωτήστε τον βοηθό σχετικά με αυτόν τον οικοδεσπότη. Οτιδήποτε αλλάζει κατάσταση χρειάζεται πρώτα την έγκρισή σας.",
"terminalPanelPlaceholder": "Ρωτήστε για αυτόν τον οικοδεσπότη...",
"noProvidersConfigured": "Δεν έχει διαμορφωθεί ακόμη πάροχος τεχνητής νοημοσύνης. Προσθέστε έναν στην καρτέλα βοηθού τεχνητής νοημοσύνης.",
"runsInOpenTerminal": "Εκτελείται στην ανοιχτή συνεδρία τερματικού σας.",
"runInTerminal": "Τρέξιμο"
},
"nav": {
"ai": "Όλα συμπεριλαμβάνονται"
},
"onboarding": {
"aiTitle": "Βοηθός Τεχνητής Νοημοσύνης",
"aiIntro": "Το Termix μπορεί να περιλαμβάνει έναν προαιρετικό βοηθό τεχνητής νοημοσύνης. Μπορείτε να το αλλάξετε αυτό αργότερα στις ρυθμίσεις σας.",
"aiSkipTitle": "Αφήστε το μακριά",
"aiSkipDesc": "Το Termix λειτουργεί όπως τώρα. Ο βοηθός παραμένει κρυφός.",
"aiEnableTitle": "Ενεργοποίησέ το",
"aiEnableDesc": "Προσθέτει έναν βοηθό που διαβάζει τη ρύθμισή σας και προτείνει αλλαγές που εγκρίνετε. Χρειάζεται έναν πάροχο που παρέχετε, όπως ένα τοπικό Ollama.",
"aiNote": "Τίποτα δεν αποστέλλεται πουθενά μέχρι να προσθέσετε έναν πάροχο και να στείλετε ένα μήνυμα."
},
"admin": {
"aiGloballyEnabled": "Βοηθός Τεχνητής Νοημοσύνης",
"aiGloballyEnabledDesc": "Επιτρέψτε στους χρήστες να ενεργοποιήσουν τον βοηθό τεχνητής νοημοσύνης. Όσο είναι απενεργοποιημένος, ο βοηθός είναι κρυφός και αποκλεισμένος για όλους.",
"aiPrivateEndpoints": "Επιτρεπόμενοι ιδιωτικοί κεντρικοί υπολογιστές τεχνητής νοημοσύνης",
"aiPrivateEndpointsDesc": "Κεντρικοί υπολογιστές στο ιδιωτικό σας δίκτυο στους οποίους οι χρήστες μπορούν να υποδείξουν έναν πάροχο, όπως ένα αυτοφιλοξενούμενο Ollama. Διαχωρίστε τους με κόμματα.",
"updateAiEnabledFailed": "Δεν ήταν δυνατή η ενημέρωση της ρύθμισης AI",
"updateAiEndpointsFailed": "Δεν ήταν δυνατή η ενημέρωση των επιτρεπόμενων κεντρικών υπολογιστών"
}
}
+78
View File
@@ -0,0 +1,78 @@
{
"ai": {
"title": "Asistente",
"send": "Enviar",
"stop": "Detener",
"save": "Ahorrar",
"cancel": "Cancelar",
"running": "correr",
"approve": "Aprobar",
"reject": "Rechazar",
"inputPlaceholder": "Pregunte sobre sus servidores o describa un cambio para proponer.",
"selectProvider": "Seleccione un proveedor",
"providerSettings": "Configuración del proveedor",
"addProvider": "Agregar proveedor",
"editProvider": "Editar proveedor",
"removeProvider": "Eliminar proveedor",
"providerType": "Proveedor",
"providerLabel": "Nombre",
"providerLabelPlaceholder": "Por ejemplo: Ollama local",
"baseUrl": "Dirección del servidor",
"apiKey": "clave API",
"defaultModel": "Modelo predeterminado",
"defaultModelPlaceholder": "Por ejemplo: llama3.1 o claude-opus-5",
"labelRequired": "Dale un nombre a este proveedor.",
"providerSaveFailed": "No se pudo guardar el proveedor",
"providerUpdated": "Proveedor actualizado",
"providerDeleteFailed": "No se pudo eliminar al proveedor.",
"privateEndpointHint": "Las direcciones de su red privada necesitan que un administrador autorice el acceso al host previamente.",
"providerOllama": "Ollama",
"providerAnthropic": "Antrópico",
"providerOpenai": "OpenAI",
"providerGemini": "Google Gemini",
"providerOpenaiCompatible": "Compatible con OpenAI",
"toolArguments": "Aporte",
"toolResult": "Resultado",
"statusApplied": "Aplicado",
"statusRejected": "Rechazado",
"proposalApplied": "Cambio aplicado",
"proposalApplyFailed": "No se pudo aplicar el cambio.",
"proposalRejectFailed": "No se pudo rechazar el cambio",
"enableTitle": "Habilitar el asistente de IA",
"enableDescription": "Deja que el asistente lea tu configuración y te sugiera cambios para que los apruebes.",
"readOnlyCommands": "Permitir comandos de diagnóstico de solo lectura",
"readOnlyCommandsDescription": "El asistente puede ejecutar una breve lista de comandos seguros, como df y uptime, sin preguntar cada vez. Cualquier cambio que afecte al servidor siempre se propone primero.",
"modelDetectFailed": "No se pudo contactar con el proveedor, por lo que esta es una lista general. Puede escribir cualquier nombre de modelo.",
"modelCustom": "Introduzca un nombre de modelo",
"modelRefresh": "Actualizar lista",
"modelPlaceholder": "Seleccione o escriba un modelo.",
"attachHint": "Escribe @ para hacer referencia a un host, fragmento de código o automatización. Se proponen cambios para su aprobación.",
"runCommandTitle": "Ejecutar comando",
"assistant": "Asistente de IA",
"terminalPanelEmpty": "Consulta con el asistente sobre este anfitrión. Cualquier cambio de estado requiere tu aprobación previa.",
"terminalPanelPlaceholder": "Pregunta por este anfitrión...",
"noProvidersConfigured": "Aún no hay ningún proveedor de IA configurado. Añada uno en la pestaña del asistente de IA.",
"runsInOpenTerminal": "Se ejecuta en tu sesión de terminal abierta.",
"runInTerminal": "Correr"
},
"nav": {
"ai": "AI"
},
"onboarding": {
"aiTitle": "Asistente de IA",
"aiIntro": "Termix puede incluir un asistente de IA opcional. Puedes cambiar esto más adelante en la configuración.",
"aiSkipTitle": "Déjalo apagado",
"aiSkipDesc": "Termix funciona como hasta ahora. El asistente permanece oculto.",
"aiEnableTitle": "Enciéndelo",
"aiEnableDesc": "Agrega un asistente que lee tu configuración y sugiere cambios que apruebas. Requiere un proveedor que tú proporciones, como un Ollama local.",
"aiNote": "No se envía nada a ningún sitio hasta que añadas un proveedor y envíes un mensaje."
},
"admin": {
"aiGloballyEnabled": "Asistente de IA",
"aiGloballyEnabledDesc": "Permita que los usuarios activen el asistente de IA. Mientras esté desactivado, el asistente permanecerá oculto y bloqueado para todos.",
"aiPrivateEndpoints": "Hosts de IA privados permitidos",
"aiPrivateEndpointsDesc": "Hosts en tu red privada a los que los usuarios pueden dirigir un proveedor, como un servidor Ollama autogestionado. Sepáralos con comas.",
"updateAiEnabledFailed": "No se pudo actualizar la configuración de la IA.",
"updateAiEndpointsFailed": "No se pudieron actualizar los hosts permitidos."
}
}
+78
View File
@@ -0,0 +1,78 @@
{
"ai": {
"title": "Assistentti",
"send": "Lähetä",
"stop": "Stop",
"save": "Tallentaa",
"cancel": "Peruuttaa",
"running": "juokseminen",
"approve": "Hyväksyä",
"reject": "Hylätä",
"inputPlaceholder": "Kysy tarjoilijoistasi tai kuvaile ehdottamaasi muutosta",
"selectProvider": "Valitse palveluntarjoaja",
"providerSettings": "Palveluntarjoajan asetukset",
"addProvider": "Lisää palveluntarjoaja",
"editProvider": "Muokkaa palveluntarjoajaa",
"removeProvider": "Poista palveluntarjoaja",
"providerType": "Palveluntarjoaja",
"providerLabel": "Nimi",
"providerLabelPlaceholder": "Esimerkiksi: Paikallinen Ollama",
"baseUrl": "Palvelimen osoite",
"apiKey": "API-avain",
"defaultModel": "Oletusmalli",
"defaultModelPlaceholder": "Esimerkiksi: llama3.1 tai claude-opus-5",
"labelRequired": "Anna tälle palveluntarjoajalle nimi",
"providerSaveFailed": "Palveluntarjoajaa ei voitu tallentaa",
"providerUpdated": "Palveluntarjoaja päivitetty",
"providerDeleteFailed": "Palveluntarjoajaa ei voitu poistaa",
"privateEndpointHint": "Yksityisen verkon osoitteiden ylläpitäjän on ensin sallittava isäntä.",
"providerOllama": "Ollama",
"providerAnthropic": "Antrooppinen",
"providerOpenai": "OpenAI",
"providerGemini": "Google Gemini",
"providerOpenaiCompatible": "OpenAI-yhteensopiva",
"toolArguments": "Syöttö",
"toolResult": "Tulos",
"statusApplied": "Käytetty",
"statusRejected": "Hylätty",
"proposalApplied": "Muutos otettu käyttöön",
"proposalApplyFailed": "Muutosta ei voitu ottaa käyttöön",
"proposalRejectFailed": "Muutosta ei voitu hylätä",
"enableTitle": "Ota tekoälyavustaja käyttöön",
"enableDescription": "Anna avustajan lukea asetuksesi ja ehdottaa muutoksia hyväksyttäväksi.",
"readOnlyCommands": "Salli vain luettavat diagnostiikkakomennot",
"readOnlyCommandsDescription": "Assistantti voi suorittaa lyhyen listan turvallisia komentoja, kuten df ja uptime, kysymättä joka kerta. Kaikki palvelinta muuttavat ehdotukset esitetään aina ensin.",
"modelDetectFailed": "Palveluntarjoajaan ei saatu yhteyttä, joten tämä on yleisluettelo. Voit kirjoittaa minkä tahansa mallin nimen.",
"modelCustom": "Anna mallin nimi",
"modelRefresh": "Päivitä lista",
"modelPlaceholder": "Valitse tai kirjoita malli",
"attachHint": "Kirjoita @ viitataksesi isäntään, koodinpätkään tai automaatioon. Muutoksia ehdotetaan hyväksyttäväksi.",
"runCommandTitle": "Suorita komento",
"assistant": "Tekoälyavustaja",
"terminalPanelEmpty": "Kysy avustajalta tästä majoittajasta. Kaikki osavaltion muutokset vaativat ensin hyväksyntäsi.",
"terminalPanelPlaceholder": "Kysy tästä isännöitsijästä...",
"noProvidersConfigured": "Tekoälypalveluntarjoajaa ei ole vielä määritetty. Lisää sellainen tekoälyavustaja-välilehdellä.",
"runsInOpenTerminal": "Suoritetaan avoimessa pääteistunnossa.",
"runInTerminal": "Juokse"
},
"nav": {
"ai": "Tekoäly"
},
"onboarding": {
"aiTitle": "Tekoälyavustaja",
"aiIntro": "Termix voi sisältää valinnaisen tekoälyavustaja. Voit muuttaa tätä myöhemmin asetuksissasi.",
"aiSkipTitle": "Jätä se pois",
"aiSkipDesc": "Termix toimii kuten nytkin. Avustaja pysyy piilossa.",
"aiEnableTitle": "Käynnistä se",
"aiEnableDesc": "Lisää avustajan, joka lukee asetuksesi ja ehdottaa hyväksymiäsi muutoksia. Edellyttää toimittamaasi palveluntarjoajaa, kuten paikallisen Ollaman.",
"aiNote": "Mitään ei lähetetä mihinkään ennen kuin lisäät palveluntarjoajan ja lähetät viestin."
},
"admin": {
"aiGloballyEnabled": "Tekoälyavustaja",
"aiGloballyEnabledDesc": "Salli käyttäjien ottaa tekoälyavustaja käyttöön. Kun tämä on pois päältä, avustaja on piilotettu ja estetty kaikilta.",
"aiPrivateEndpoints": "Sallitut yksityiset tekoälyisännät",
"aiPrivateEndpointsDesc": "Yksityisen verkon isännät, joihin käyttäjät voivat ohjata palveluntarjoajan, kuten itse isännöity Ollama. Erota ne pilkuilla.",
"updateAiEnabledFailed": "Tekoälyasetuksen päivittäminen epäonnistui",
"updateAiEndpointsFailed": "Sallittujen isäntien päivittäminen epäonnistui"
}
}
+78
View File
@@ -0,0 +1,78 @@
{
"ai": {
"title": "Assistant",
"send": "Envoyer",
"stop": "Arrêt",
"save": "Sauvegarder",
"cancel": "Annuler",
"running": "en cours d'exécution",
"approve": "Approuver",
"reject": "Rejeter",
"inputPlaceholder": "Renseignez-vous sur vos serveurs ou décrivez un changement à proposer.",
"selectProvider": "Sélectionnez un fournisseur",
"providerSettings": "Paramètres du fournisseur",
"addProvider": "Ajouter un fournisseur",
"editProvider": "Modifier le fournisseur",
"removeProvider": "Supprimer le fournisseur",
"providerType": "Fournisseur",
"providerLabel": "Nom",
"providerLabelPlaceholder": "Par exemple : Ollama local",
"baseUrl": "Adresse du serveur",
"apiKey": "Clé API",
"defaultModel": "Modèle par défaut",
"defaultModelPlaceholder": "Par exemple : llama3.1 ou claude-opus-5",
"labelRequired": "Donnez un nom à ce fournisseur",
"providerSaveFailed": "Impossible de sauver le fournisseur",
"providerUpdated": "Mise à jour du fournisseur",
"providerDeleteFailed": "Impossible de supprimer le fournisseur",
"privateEndpointHint": "Les adresses de votre réseau privé nécessitent l'autorisation préalable d'un administrateur pour accéder à l'hôte.",
"providerOllama": "Ollama",
"providerAnthropic": "Anthropique",
"providerOpenai": "OpenAI",
"providerGemini": "Google Gemini",
"providerOpenaiCompatible": "Compatible avec OpenAI",
"toolArguments": "Saisir",
"toolResult": "Résultat",
"statusApplied": "Appliqué",
"statusRejected": "Rejeté",
"proposalApplied": "Changement appliqué",
"proposalApplyFailed": "Impossible d'appliquer la modification",
"proposalRejectFailed": "Impossible de refuser la modification",
"enableTitle": "Activer l'assistant IA",
"enableDescription": "Laissez l'assistant analyser votre configuration et vous suggérer des modifications à approuver.",
"readOnlyCommands": "Autoriser les commandes de diagnostic en lecture seule",
"readOnlyCommandsDescription": "L'assistant peut exécuter une courte liste de commandes sûres, comme `df` et `uptime`, sans demander l'autorisation à chaque fois. Toute modification apportée au serveur est toujours proposée au préalable.",
"modelDetectFailed": "Impossible de joindre le fournisseur ; voici donc une liste générale. Vous pouvez saisir n’importe quel nom de modèle.",
"modelCustom": "Saisissez le nom du modèle",
"modelRefresh": "Actualiser la liste",
"modelPlaceholder": "Choisissez ou saisissez un modèle",
"attachHint": "Saisissez @ pour faire référence à un hôte, un extrait de code ou une automatisation. Les modifications proposées doivent être approuvées.",
"runCommandTitle": "Exécuter la commande",
"assistant": "Assistant IA",
"terminalPanelEmpty": "Renseignez-vous auprès de l'assistant concernant cet hôte. Tout changement d'état nécessite votre approbation préalable.",
"terminalPanelPlaceholder": "Renseignez-vous sur cet hôte...",
"noProvidersConfigured": "Aucun fournisseur d'IA n'est encore configuré. Ajoutez-en un dans l'onglet Assistant IA.",
"runsInOpenTerminal": "S'exécute dans votre session de terminal ouverte.",
"runInTerminal": "Courir"
},
"nav": {
"ai": "IA"
},
"onboarding": {
"aiTitle": "Assistant IA",
"aiIntro": "Termix peut inclure un assistant IA optionnel. Vous pouvez modifier ce paramètre ultérieurement.",
"aiSkipTitle": "Laissez-le de côté",
"aiSkipDesc": "Termix fonctionne comme actuellement. L'assistant reste caché.",
"aiEnableTitle": "Allumez-le",
"aiEnableDesc": "Ajoute un assistant qui analyse votre configuration et suggère des modifications que vous approuvez. Nécessite un fournisseur que vous désignez, comme un Ollama local.",
"aiNote": "Aucun message n'est envoyé tant que vous n'avez pas ajouté de fournisseur et envoyé un message."
},
"admin": {
"aiGloballyEnabled": "Assistant IA",
"aiGloballyEnabledDesc": "Autoriser les utilisateurs à activer l'assistant IA. Lorsqu'il est désactivé, l'assistant est masqué et inaccessible à tous.",
"aiPrivateEndpoints": "Hôtes d'IA privés autorisés",
"aiPrivateEndpointsDesc": "Les hôtes de votre réseau privé vers lesquels les utilisateurs peuvent orienter un fournisseur, comme un serveur Ollama auto-hébergé. Séparez-les par des virgules.",
"updateAiEnabledFailed": "Impossible de mettre à jour les paramètres d'IA",
"updateAiEndpointsFailed": "Impossible de mettre à jour les hôtes autorisés."
}
}
+78
View File
@@ -0,0 +1,78 @@
{
"ai": {
"title": "עוֹזֵר",
"send": "לִשְׁלוֹחַ",
"stop": "לְהַפְסִיק",
"save": "לְהַצִיל",
"cancel": "לְבַטֵל",
"running": "רִיצָה",
"approve": "לְאַשֵׁר",
"reject": "לִדחוֹת",
"inputPlaceholder": "שאל לגבי המלצרים שלך, או תאר שינוי להציע",
"selectProvider": "בחר ספק",
"providerSettings": "הגדרות ספק",
"addProvider": "הוסף ספק",
"editProvider": "עריכת ספק",
"removeProvider": "הסר ספק",
"providerType": "ספק",
"providerLabel": "שֵׁם",
"providerLabelPlaceholder": "לדוגמה: אולמה מקומית",
"baseUrl": "כתובת השרת",
"apiKey": "מפתח API",
"defaultModel": "מודל ברירת מחדל",
"defaultModelPlaceholder": "לדוגמה: llama3.1 או claude-opus-5",
"labelRequired": "תן שם לספק הזה",
"providerSaveFailed": "לא ניתן היה לשמור את הספק",
"providerUpdated": "הספק עודכן",
"providerDeleteFailed": "לא ניתן היה להסיר את הספק",
"privateEndpointHint": "כתובות ברשת הפרטית שלך דורשות תחילה אישור ממנהל מערכת למארח.",
"providerOllama": "אולמה",
"providerAnthropic": "אנתרופי",
"providerOpenai": "בינה מלאכותית פתוחה",
"providerGemini": "גוגל ג'מיני",
"providerOpenaiCompatible": "תואם ל-OpenAI",
"toolArguments": "קֶלֶט",
"toolResult": "תוֹצָאָה",
"statusApplied": "יישומי",
"statusRejected": "נִדחֶה",
"proposalApplied": "השינוי הוחל",
"proposalApplyFailed": "לא ניתן היה להחיל את השינוי",
"proposalRejectFailed": "לא ניתן היה לדחות את השינוי",
"enableTitle": "הפעל את עוזר הבינה המלאכותית",
"enableDescription": "תן לעוזר לקרוא את ההגדרות שלך ולהציע שינויים לאישור.",
"readOnlyCommands": "אפשר פקודות אבחון לקריאה בלבד",
"readOnlyCommandsDescription": "העוזר יכול להריץ רשימה קצרה של פקודות בטוחות כמו df ו-uptime מבלי לשאול בכל פעם. כל דבר שמשנה שרת תמיד מוצע ראשון.",
"modelDetectFailed": "לא ניתן היה להגיע לספק, לכן זוהי רשימה כללית. ניתן להקליד כל שם דגם.",
"modelCustom": "הזן שם דגם",
"modelRefresh": "רענון הרשימה",
"modelPlaceholder": "בחר או הקלד מודל",
"attachHint": "הקלד @ כדי להפנות למארח, קטע טקסט או אוטומציה. שינויים מוצעים לאישורך.",
"runCommandTitle": "הפעלה של הפקודה",
"assistant": "עוזר בינה מלאכותית",
"terminalPanelEmpty": "שאל את העוזר לגבי המארח הזה. כל שינוי במצב דורש תחילה את אישורך.",
"terminalPanelPlaceholder": "שאל על המארח הזה...",
"noProvidersConfigured": "עדיין לא הוגדר ספק בינה מלאכותית. הוסף אחד בכרטיסייה עוזר בינה מלאכותית.",
"runsInOpenTerminal": "פועל בסשן הטרמינל הפתוח שלך.",
"runInTerminal": "לָרוּץ"
},
"nav": {
"ai": "בינה מלאכותית"
},
"onboarding": {
"aiTitle": "עוזר בינה מלאכותית",
"aiIntro": "טרמיקס יכול לכלול עוזר בינה מלאכותית אופציונלי. ניתן לשנות זאת מאוחר יותר בהגדרות.",
"aiSkipTitle": "תעזוב את זה",
"aiSkipDesc": "טרמיקס עובד כמו שהוא עובד עכשיו. העוזר נשאר מוסתר.",
"aiEnableTitle": "הפעל אותו",
"aiEnableDesc": "מוסיף עוזר שקורא את ההגדרות שלך ומציע שינויים שאתה מאשר. נדרש ספק שאתה מספק, כגון אולמה מקומי.",
"aiNote": "שום דבר לא נשלח לשום מקום עד שתוסיף ספק ותשלח הודעה."
},
"admin": {
"aiGloballyEnabled": "עוזר בינה מלאכותית",
"aiGloballyEnabledDesc": "אפשר למשתמשים להפעיל את עוזר הבינה המלאכותית. כאשר זה כבוי, העוזר מוסתר וחסום עבור כולם.",
"aiPrivateEndpoints": "מארחים פרטיים של בינה מלאכותית מורשים",
"aiPrivateEndpointsDesc": "מארחים ברשת הפרטית שלך שמשתמשים עשויים להפנות אליהם ספק, כגון Ollama המאוחסן בעצמו. הפרד ביניהם באמצעות פסיקים.",
"updateAiEnabledFailed": "לא ניתן היה לעדכן את הגדרת הבינה המלאכותית",
"updateAiEndpointsFailed": "לא ניתן היה לעדכן את המארחים המותרים"
}
}
+78
View File
@@ -0,0 +1,78 @@
{
"ai": {
"title": "सहायक",
"send": "भेजना",
"stop": "रुकना",
"save": "बचाना",
"cancel": "रद्द करना",
"running": "दौड़ना",
"approve": "मंज़ूरी देना",
"reject": "अस्वीकार करना",
"inputPlaceholder": "अपने सर्वरों के बारे में पूछें, या प्रस्तावित परिवर्तन का वर्णन करें।",
"selectProvider": "एक प्रदाता का चयन करें",
"providerSettings": "प्रदाता सेटिंग्स",
"addProvider": "प्रदाता जोड़ें",
"editProvider": "प्रदाता संपादित करें",
"removeProvider": "प्रदाता को हटाएँ",
"providerType": "प्रदाता",
"providerLabel": "नाम",
"providerLabelPlaceholder": "उदाहरण के लिए: स्थानीय ओलामा",
"baseUrl": "सर्वर पता",
"apiKey": "एपीआई कुंजी",
"defaultModel": "डिफ़ॉल्ट मॉडल",
"defaultModelPlaceholder": "उदाहरण के लिए: llama3.1 या claude-opus-5",
"labelRequired": "इस प्रदाता को एक नाम दें",
"providerSaveFailed": "प्रदाता को बचाया नहीं जा सका",
"providerUpdated": "प्रदाता ने अपडेट किया",
"providerDeleteFailed": "प्रदाता को हटाया नहीं जा सका",
"privateEndpointHint": "आपके निजी नेटवर्क पर मौजूद पतों के लिए पहले एक प्रशासक द्वारा होस्ट को अनुमति देना आवश्यक है।",
"providerOllama": "ओलामा",
"providerAnthropic": "anthropic",
"providerOpenai": "ओपनएआई",
"providerGemini": "गूगल जेमिनी",
"providerOpenaiCompatible": "OpenAI संगत",
"toolArguments": "इनपुट",
"toolResult": "परिणाम",
"statusApplied": "लागू किया गया",
"statusRejected": "अस्वीकार कर दिया",
"proposalApplied": "परिवर्तन लागू हो गया",
"proposalApplyFailed": "परिवर्तन लागू नहीं हो सका",
"proposalRejectFailed": "परिवर्तन को अस्वीकार नहीं किया जा सका",
"enableTitle": "एआई सहायक को सक्रिय करें",
"enableDescription": "सहायक को आपकी सेटअप प्रक्रिया पढ़ने दें और वह आपके अनुमोदन के लिए परिवर्तन सुझाएगा।",
"readOnlyCommands": "केवल पढ़ने योग्य डायग्नोस्टिक कमांड की अनुमति दें",
"readOnlyCommandsDescription": "सहायक बिना पूछे ही df और uptime जैसे कुछ सुरक्षित कमांड चला सकता है। सर्वर में कोई भी बदलाव करने का प्रस्ताव हमेशा पहले दिया जाता है।",
"modelDetectFailed": "प्रदाता से संपर्क नहीं हो सका, इसलिए यह एक सामान्य सूची है। आप किसी भी मॉडल का नाम टाइप कर सकते हैं।",
"modelCustom": "मॉडल का नाम दर्ज करें",
"modelRefresh": "सूची रीफ़्रेश करें",
"modelPlaceholder": "कोई मॉडल चुनें या टाइप करें",
"attachHint": "किसी होस्ट, स्निपेट या ऑटोमेशन को संदर्भित करने के लिए @ टाइप करें। आपके अनुमोदन के लिए परिवर्तन प्रस्तावित किए जा रहे हैं।",
"runCommandTitle": "कमांड चलाएँ",
"assistant": "एआई सहायक",
"terminalPanelEmpty": "इस होस्ट के बारे में असिस्टेंट से पूछें। किसी भी स्थिति में बदलाव के लिए पहले आपकी स्वीकृति आवश्यक है।",
"terminalPanelPlaceholder": "इस होस्ट के बारे में पूछें...",
"noProvidersConfigured": "अभी तक कोई एआई प्रदाता कॉन्फ़िगर नहीं किया गया है। एआई सहायक टैब में एक जोड़ें।",
"runsInOpenTerminal": "यह आपके खुले टर्मिनल सत्र में चलता है।",
"runInTerminal": "दौड़ना"
},
"nav": {
"ai": "ऐ"
},
"onboarding": {
"aiTitle": "एआई सहायक",
"aiIntro": "टर्मिक्स में एक वैकल्पिक एआई सहायक शामिल हो सकता है। आप इसे बाद में अपनी सेटिंग्स में बदल सकते हैं।",
"aiSkipTitle": "इसे बंद कर दो",
"aiSkipDesc": "टर्मिक्स अभी की तरह ही काम करता है। असिस्टेंट छिपा रहता है।",
"aiEnableTitle": "इसे चालू करें",
"aiEnableDesc": "यह एक सहायक सुविधा जोड़ता है जो आपके सेटअप को पढ़ता है और आपके द्वारा स्वीकृत किए जाने वाले परिवर्तनों का सुझाव देता है। इसके लिए आपको एक प्रदाता उपलब्ध कराना होगा, जैसे कि स्थानीय ओलामा।",
"aiNote": "जब तक आप कोई प्रदाता नहीं जोड़ते और संदेश नहीं भेजते, तब तक कुछ भी कहीं नहीं भेजा जाता।"
},
"admin": {
"aiGloballyEnabled": "एआई सहायक",
"aiGloballyEnabledDesc": "उपयोगकर्ताओं को एआई सहायक चालू करने की अनुमति दें। जब यह बंद होगा, तो सहायक सभी के लिए छिपा हुआ और अवरुद्ध रहेगा।",
"aiPrivateEndpoints": "निजी एआई होस्ट की अनुमति दी गई",
"aiPrivateEndpointsDesc": "आपके निजी नेटवर्क पर मौजूद होस्ट, जिन्हें उपयोगकर्ता किसी प्रदाता से जोड़ सकते हैं, जैसे कि स्व-होस्टेड ओलामा। इन्हें अल्पविराम से अलग करें।",
"updateAiEnabledFailed": "एआई सेटिंग अपडेट नहीं हो सकी",
"updateAiEndpointsFailed": "अनुमत होस्टों को अपडेट नहीं किया जा सका"
}
}
+78
View File
@@ -0,0 +1,78 @@
{
"ai": {
"title": "Helyettes",
"send": "Elküld",
"stop": "Stop",
"save": "Megtakarítás",
"cancel": "Mégsem",
"running": "futás",
"approve": "Jóváhagyás",
"reject": "Elutasít",
"inputPlaceholder": "Kérdezzen a felszolgálókról, vagy írjon le egy javasolt változtatást",
"selectProvider": "Válasszon szolgáltatót",
"providerSettings": "Szolgáltatói beállítások",
"addProvider": "Szolgáltató hozzáadása",
"editProvider": "Szolgáltató szerkesztése",
"removeProvider": "Szolgáltató eltávolítása",
"providerType": "Szolgáltató",
"providerLabel": "Név",
"providerLabelPlaceholder": "Például: Helyi Ollama",
"baseUrl": "Szerver címe",
"apiKey": "API-kulcs",
"defaultModel": "Alapértelmezett modell",
"defaultModelPlaceholder": "Például: llama3.1 vagy claude-opus-5",
"labelRequired": "Adjon nevet ennek a szolgáltatónak",
"providerSaveFailed": "Nem sikerült menteni a szolgáltatót",
"providerUpdated": "Szolgáltató frissítve",
"providerDeleteFailed": "Nem sikerült eltávolítani a szolgáltatót",
"privateEndpointHint": "A privát hálózaton lévő címekhez először egy rendszergazdának kell engedélyeznie a gazdagépet.",
"providerOllama": "Ollama",
"providerAnthropic": "Antropikus",
"providerOpenai": "Nyílt mesterséges intelligencia",
"providerGemini": "Google Gemini",
"providerOpenaiCompatible": "OpenAI-kompatibilis",
"toolArguments": "Bemenet",
"toolResult": "Eredmény",
"statusApplied": "Alkalmazott",
"statusRejected": "Elutasítva",
"proposalApplied": "Módosítás alkalmazva",
"proposalApplyFailed": "Nem sikerült alkalmazni a módosítást",
"proposalRejectFailed": "Nem sikerült elutasítani a módosítást",
"enableTitle": "Engedélyezze a mesterséges intelligencia asszisztenst",
"enableDescription": "Hagyd, hogy az asszisztens felolvassa a beállításokat, és javasoljon módosításokat jóváhagyásra.",
"readOnlyCommands": "Csak olvasható diagnosztikai parancsok engedélyezése",
"readOnlyCommandsDescription": "Az asszisztens lefuttathat egy rövid listát a biztonságos parancsokról, mint például a df és az uptime, anélkül, hogy minden alkalommal rákérdezne. Bármi, ami megváltoztat egy szervert, mindig elsőként kerül felkérésre.",
"modelDetectFailed": "Nem sikerült elérni a szolgáltatót, ezért ez egy általános lista. Bármelyik modellnevet beírhatja.",
"modelCustom": "Adjon meg egy modellnevet",
"modelRefresh": "Lista frissítése",
"modelPlaceholder": "Válasszon vagy írjon be egy modellt",
"attachHint": "Írjon be @ jelet egy gazdagépre, kódrészletre vagy automatizálásra való hivatkozáshoz. A rendszer módosításokat javasol jóváhagyásra.",
"runCommandTitle": "Parancs futtatása",
"assistant": "MI asszisztens",
"terminalPanelEmpty": "Kérdezd meg az asszisztenst erről a házigazdáról. Bármi, ami megváltoztatja az állapotát, először a jóváhagyásodra van szükség.",
"terminalPanelPlaceholder": "Kérdezz rá erre a házigazdára...",
"noProvidersConfigured": "Még nincs konfigurálva AI-szolgáltató. Adjon hozzá egyet az AI-asszisztens fülön.",
"runsInOpenTerminal": "A megnyitott terminál munkamenetben fut.",
"runInTerminal": "Fut"
},
"nav": {
"ai": "MI"
},
"onboarding": {
"aiTitle": "MI asszisztens",
"aiIntro": "A Termix tartalmazhat opcionális mesterséges intelligencia asszisztenst. Ezt később módosíthatja a beállításokban.",
"aiSkipTitle": "Hagyd ki",
"aiSkipDesc": "A Termix ugyanúgy működik, mint most. Az asszisztens rejtve marad.",
"aiEnableTitle": "Kapcsold be",
"aiEnableDesc": "Hozzáad egy asszisztenst, amely beolvassa a beállításait, és javaslatokat tesz az Ön által jóváhagyott módosításokra. Szükség van egy Ön által biztosított szolgáltatóra, például egy helyi Ollama szakemberre.",
"aiNote": "Semmi sem kerül sehova, amíg nem adsz hozzá egy szolgáltatót és nem küldesz üzenetet."
},
"admin": {
"aiGloballyEnabled": "MI asszisztens",
"aiGloballyEnabledDesc": "Engedélyezheti a felhasználóknak a mesterséges intelligencia asszisztens bekapcsolását. Amíg ez ki van kapcsolva, az asszisztens rejtett és blokkolt mindenki számára.",
"aiPrivateEndpoints": "Engedélyezett privát AI-gazdagépek",
"aiPrivateEndpointsDesc": "A privát hálózatodon található olyan hosztok, amelyekre a felhasználók szolgáltatót irányíthatnak, például egy saját hosztolt Ollama. Vesszővel válaszd el őket.",
"updateAiEnabledFailed": "Nem sikerült frissíteni a mesterséges intelligencia beállítását",
"updateAiEndpointsFailed": "Nem sikerült frissíteni az engedélyezett gazdagépeket"
}
}
+78
View File
@@ -0,0 +1,78 @@
{
"ai": {
"title": "Asisten",
"send": "Mengirim",
"stop": "Berhenti",
"save": "Menyimpan",
"cancel": "Membatalkan",
"running": "berlari",
"approve": "Menyetujui",
"reject": "Menolak",
"inputPlaceholder": "Tanyakan tentang server Anda, atau jelaskan perubahan yang ingin Anda usulkan.",
"selectProvider": "Pilih penyedia",
"providerSettings": "Pengaturan penyedia",
"addProvider": "Tambahkan penyedia",
"editProvider": "Penyedia pengeditan",
"removeProvider": "Hapus penyedia",
"providerType": "Penyedia",
"providerLabel": "Nama",
"providerLabelPlaceholder": "Contohnya: Ollama setempat",
"baseUrl": "Alamat server",
"apiKey": "Kunci API",
"defaultModel": "Model default",
"defaultModelPlaceholder": "Misalnya: llama3.1 atau claude-opus-5",
"labelRequired": "Beri nama penyedia ini",
"providerSaveFailed": "Penyedia layanan tidak dapat disimpan.",
"providerUpdated": "Penyedia diperbarui",
"providerDeleteFailed": "Penyedia layanan tidak dapat dihapus.",
"privateEndpointHint": "Alamat-alamat di jaringan pribadi Anda memerlukan izin administrator terlebih dahulu untuk host tersebut.",
"providerOllama": "Ollama",
"providerAnthropic": "Antropis",
"providerOpenai": "OpenAI",
"providerGemini": "Google Gemini",
"providerOpenaiCompatible": "Kompatibel dengan OpenAI",
"toolArguments": "Masukan",
"toolResult": "Hasil",
"statusApplied": "Terapan",
"statusRejected": "Ditolak",
"proposalApplied": "Perubahan diterapkan",
"proposalApplyFailed": "Perubahan tidak dapat diterapkan.",
"proposalRejectFailed": "Tidak bisa menolak perubahan tersebut",
"enableTitle": "Aktifkan asisten AI",
"enableDescription": "Biarkan asisten membaca pengaturan Anda dan menyarankan perubahan yang perlu Anda setujui.",
"readOnlyCommands": "Izinkan perintah diagnostik hanya baca.",
"readOnlyCommandsDescription": "Asisten dapat menjalankan daftar singkat perintah aman seperti df dan uptime tanpa bertanya setiap kali. Apa pun yang mengubah server selalu diusulkan terlebih dahulu.",
"modelDetectFailed": "Tidak dapat menghubungi penyedia, jadi ini adalah daftar umum. Anda dapat mengetikkan nama model apa pun.",
"modelCustom": "Masukkan nama model",
"modelRefresh": "Perbarui daftar",
"modelPlaceholder": "Pilih atau ketik model",
"attachHint": "Ketik @ untuk merujuk ke host, cuplikan kode, atau otomatisasi. Perubahan diajukan untuk Anda setujui.",
"runCommandTitle": "Jalankan perintah",
"assistant": "Asisten AI",
"terminalPanelEmpty": "Tanyakan kepada asisten tentang tuan rumah ini. Segala sesuatu yang berubah status memerlukan persetujuan Anda terlebih dahulu.",
"terminalPanelPlaceholder": "Tanyakan tentang tuan rumah ini...",
"noProvidersConfigured": "Belum ada penyedia AI yang dikonfigurasi. Tambahkan satu di tab asisten AI.",
"runsInOpenTerminal": "Berjalan di sesi terminal Anda yang sedang terbuka.",
"runInTerminal": "Berlari"
},
"nav": {
"ai": "AI"
},
"onboarding": {
"aiTitle": "Asisten AI",
"aiIntro": "Termix dapat menyertakan asisten AI opsional. Anda dapat mengubahnya nanti di pengaturan Anda.",
"aiSkipTitle": "Biarkan saja",
"aiSkipDesc": "Termix berfungsi seperti sekarang. Asisten tetap tersembunyi.",
"aiEnableTitle": "Nyalakan",
"aiEnableDesc": "Menambahkan asisten yang membaca pengaturan Anda dan menyarankan perubahan yang Anda setujui. Membutuhkan penyedia yang Anda berikan, seperti Ollama lokal.",
"aiNote": "Tidak ada data yang dikirim ke mana pun sampai Anda menambahkan penyedia dan mengirim pesan."
},
"admin": {
"aiGloballyEnabled": "Asisten AI",
"aiGloballyEnabledDesc": "Izinkan pengguna untuk mengaktifkan asisten AI. Saat dinonaktifkan, asisten akan disembunyikan dan diblokir untuk semua orang.",
"aiPrivateEndpoints": "Host AI pribadi yang diizinkan",
"aiPrivateEndpointsDesc": "Host di jaringan pribadi Anda yang mungkin ditunjuk pengguna untuk penyedia layanan, seperti Ollama yang dihosting sendiri. Pisahkan dengan koma.",
"updateAiEnabledFailed": "Pengaturan AI tidak dapat diperbarui.",
"updateAiEndpointsFailed": "Tidak dapat memperbarui host yang diizinkan."
}
}
+78
View File
@@ -0,0 +1,78 @@
{
"ai": {
"title": "Assistente",
"send": "Inviare",
"stop": "Fermare",
"save": "Salva",
"cancel": "Cancellare",
"running": "corsa",
"approve": "Approvare",
"reject": "Rifiutare",
"inputPlaceholder": "Chiedi informazioni sui tuoi server o descrivi una modifica da proporre",
"selectProvider": "Seleziona un fornitore",
"providerSettings": "Impostazioni del provider",
"addProvider": "Aggiungi fornitore",
"editProvider": "Fornitore di modifica",
"removeProvider": "Rimuovi il fornitore",
"providerType": "Fornitore",
"providerLabel": "Nome",
"providerLabelPlaceholder": "Ad esempio: Ollama locale",
"baseUrl": "Indirizzo del server",
"apiKey": "Chiave API",
"defaultModel": "Modello predefinito",
"defaultModelPlaceholder": "Ad esempio: llama3.1 o claude-opus-5",
"labelRequired": "Assegna un nome a questo fornitore",
"providerSaveFailed": "Impossibile salvare il provider",
"providerUpdated": "Fornitore aggiornato",
"providerDeleteFailed": "Impossibile rimuovere il provider",
"privateEndpointHint": "Gli indirizzi sulla tua rete privata richiedono prima l'autorizzazione di un amministratore per l'host.",
"providerOllama": "Ollama",
"providerAnthropic": "Antropico",
"providerOpenai": "OpenAI",
"providerGemini": "Google Gemini",
"providerOpenaiCompatible": "Compatibile con OpenAI",
"toolArguments": "Ingresso",
"toolResult": "Risultato",
"statusApplied": "Applicato",
"statusRejected": "Respinto",
"proposalApplied": "Modifica applicata",
"proposalApplyFailed": "Impossibile applicare la modifica",
"proposalRejectFailed": "Impossibile rifiutare la modifica",
"enableTitle": "Abilita l'assistente IA",
"enableDescription": "Lascia che l'assistente legga la tua configurazione e ti suggerisca delle modifiche da approvare.",
"readOnlyCommands": "Consenti comandi diagnostici di sola lettura",
"readOnlyCommandsDescription": "L'assistente può eseguire un breve elenco di comandi sicuri come df e uptime senza chiedere conferma ogni volta. Qualsiasi operazione che modifichi un server viene sempre proposta in anticipo.",
"modelDetectFailed": "Non è stato possibile contattare il fornitore, quindi questo è un elenco generico. È possibile digitare qualsiasi nome di modello.",
"modelCustom": "Inserisci il nome del modello",
"modelRefresh": "Aggiorna elenco",
"modelPlaceholder": "Scegli o digita un modello",
"attachHint": "Digita @ per fare riferimento a un host, uno snippet o un'automazione. Le modifiche proposte sono da approvare.",
"runCommandTitle": "Esegui il comando",
"assistant": "Assistente AI",
"terminalPanelEmpty": "Chiedi all'assistente informazioni su questo host. Qualsiasi modifica allo stato richiede la tua approvazione preventiva.",
"terminalPanelPlaceholder": "Chiedi informazioni su questo host...",
"noProvidersConfigured": "Non è ancora configurato alcun provider di IA. Aggiungine uno nella scheda Assistente IA.",
"runsInOpenTerminal": "Viene eseguito nella sessione del terminale aperta.",
"runInTerminal": "Correre"
},
"nav": {
"ai": "IA"
},
"onboarding": {
"aiTitle": "Assistente basato sull'intelligenza artificiale",
"aiIntro": "Termix può includere un assistente AI opzionale. Puoi modificare questa impostazione in seguito.",
"aiSkipTitle": "Lascialo spento",
"aiSkipDesc": "Termix funziona come ora. L'assistente rimane nascosto.",
"aiEnableTitle": "Accendilo",
"aiEnableDesc": "Aggiunge un assistente che legge la tua configurazione e suggerisce modifiche che approvi. Richiede un provider che tu fornisca, come ad esempio un Ollama locale.",
"aiNote": "Non viene inviato nulla finché non si aggiunge un provider e si invia un messaggio."
},
"admin": {
"aiGloballyEnabled": "Assistente basato sull'intelligenza artificiale",
"aiGloballyEnabledDesc": "Consenti agli utenti di attivare l'assistente AI. Quando è disattivato, l'assistente è nascosto e bloccato per tutti.",
"aiPrivateEndpoints": "Host di IA privati consentiti",
"aiPrivateEndpointsDesc": "Host sulla tua rete privata a cui gli utenti possono indirizzare un provider, come ad esempio un'istanza di Ollama autogestita. Separali con delle virgole.",
"updateAiEnabledFailed": "Impossibile aggiornare le impostazioni dell'IA",
"updateAiEndpointsFailed": "Impossibile aggiornare gli host consentiti"
}
}
+78
View File
@@ -0,0 +1,78 @@
{
"ai": {
"title": "アシスタント",
"send": "送信",
"stop": "停止",
"save": "保存",
"cancel": "キャンセル",
"running": "ランニング",
"approve": "承認する",
"reject": "拒否する",
"inputPlaceholder": "サーバーについて質問するか、変更案を説明する",
"selectProvider": "プロバイダーを選択してください",
"providerSettings": "プロバイダー設定",
"addProvider": "プロバイダーを追加",
"editProvider": "プロバイダーを編集",
"removeProvider": "プロバイダーを削除",
"providerType": "プロバイダー",
"providerLabel": "名前",
"providerLabelPlaceholder": "例えば:地元のオラマ",
"baseUrl": "サーバーアドレス",
"apiKey": "APIキー",
"defaultModel": "デフォルトモデル",
"defaultModelPlaceholder": "例えば、llama3.1 や claude-opus-5 など。",
"labelRequired": "このプロバイダーに名前を付けてください",
"providerSaveFailed": "プロバイダーを保存できませんでした",
"providerUpdated": "プロバイダーが更新されました",
"providerDeleteFailed": "プロバイダーを削除できませんでした",
"privateEndpointHint": "プライベートネットワーク上のアドレスを使用するには、まず管理者がホストを許可する必要があります。",
"providerOllama": "オラマ",
"providerAnthropic": "人間",
"providerOpenai": "OpenAI",
"providerGemini": "Google Gemini",
"providerOpenaiCompatible": "OpenAI対応",
"toolArguments": "入力",
"toolResult": "結果",
"statusApplied": "適用済み",
"statusRejected": "拒否されました",
"proposalApplied": "変更が適用されました",
"proposalApplyFailed": "変更を適用できませんでした",
"proposalRejectFailed": "変更を拒否できませんでした",
"enableTitle": "AIアシスタントを有効にする",
"enableDescription": "アシスタントにあなたの設定を読み取らせ、承認が必要な変更案を提案させましょう。",
"readOnlyCommands": "読み取り専用の診断コマンドを許可する",
"readOnlyCommandsDescription": "アシスタントは、dfやuptimeといった安全なコマンドを毎回確認することなく実行する場合があります。サーバーに変更を加える操作は、常に最初に提案されます。",
"modelDetectFailed": "プロバイダーに連絡が取れなかったため、これは一般的なリストです。任意のモデル名を入力してください。",
"modelCustom": "モデル名を入力してください",
"modelRefresh": "リストを更新",
"modelPlaceholder": "モデルを選択または入力してください",
"attachHint": "ホスト、スニペット、またはオートメーションを参照するには、「@」を入力してください。変更案が表示され、承認が必要となります。",
"runCommandTitle": "コマンドを実行する",
"assistant": "AIアシスタント",
"terminalPanelEmpty": "このホストについてアシスタントにお問い合わせください。状態変更を伴う変更はすべて、事前にあなたの承認が必要です。",
"terminalPanelPlaceholder": "このホストについて質問する...",
"noProvidersConfigured": "AIプロバイダーがまだ設定されていません。AIアシスタントタブで追加してください。",
"runsInOpenTerminal": "開いているターミナルセッションで実行されます。",
"runInTerminal": "走る"
},
"nav": {
"ai": "AI"
},
"onboarding": {
"aiTitle": "AIアシスタント",
"aiIntro": "TermixにはオプションでAIアシスタントを搭載できます。これは後で設定画面で変更できます。",
"aiSkipTitle": "オフにしておく",
"aiSkipDesc": "Termixは現状のまま動作します。アシスタントは非表示のままです。",
"aiEnableTitle": "電源を入れる",
"aiEnableDesc": "設定内容を読み取り、承認した上で変更案を提案するアシスタント機能を追加します。ローカルのOllamaなど、お客様ご自身で手配するプロバイダーが必要です。",
"aiNote": "プロバイダーを追加してメッセージを送信するまで、データはどこにも送信されません。"
},
"admin": {
"aiGloballyEnabled": "AIアシスタント",
"aiGloballyEnabledDesc": "ユーザーがAIアシスタントをオンにできるようにします。オフになっている間は、アシスタントは非表示になり、誰からも利用できなくなります。",
"aiPrivateEndpoints": "プライベートAIホストを許可",
"aiPrivateEndpointsDesc": "ユーザーがプロバイダに指定できる、プライベートネットワーク上のホスト(自己ホスト型のOllamaなど)を指定します。カンマで区切ってください。",
"updateAiEnabledFailed": "AI設定を更新できませんでした",
"updateAiEndpointsFailed": "許可されたホストを更新できませんでした"
}
}
+78
View File
@@ -0,0 +1,78 @@
{
"ai": {
"title": "어시스턴트",
"send": "보내다",
"stop": "멈추다",
"save": "구하다",
"cancel": "취소",
"running": "달리기",
"approve": "승인하다",
"reject": "거부하다",
"inputPlaceholder": "서버에 대해 문의하거나, 변경 사항을 제안해 보세요.",
"selectProvider": "제공업체를 선택하세요",
"providerSettings": "공급자 설정",
"addProvider": "공급자 추가",
"editProvider": "제공자 수정",
"removeProvider": "제공업체를 제거하세요",
"providerType": "공급자",
"providerLabel": "이름",
"providerLabelPlaceholder": "예: 지역 올라마",
"baseUrl": "서버 주소",
"apiKey": "API 키",
"defaultModel": "기본 모델",
"defaultModelPlaceholder": "예를 들어: llama3.1 또는 claude-opus-5",
"labelRequired": "이 제공업체에 이름을 지정하세요",
"providerSaveFailed": "공급자를 저장할 수 없습니다.",
"providerUpdated": "제공업체 업데이트됨",
"providerDeleteFailed": "공급자를 제거할 수 없습니다.",
"privateEndpointHint": "개인 네트워크의 주소에 접근하려면 관리자가 먼저 호스트를 허용해야 합니다.",
"providerOllama": "올라마",
"providerAnthropic": "인류",
"providerOpenai": "오픈아이",
"providerGemini": "구글 제미니",
"providerOpenaiCompatible": "OpenAI와 호환 가능",
"toolArguments": "입력",
"toolResult": "결과",
"statusApplied": "적용된",
"statusRejected": "거절됨",
"proposalApplied": "변경 사항이 적용되었습니다",
"proposalApplyFailed": "변경 사항을 적용할 수 없습니다.",
"proposalRejectFailed": "변경을 거부할 수 없었습니다",
"enableTitle": "AI 비서를 활성화하세요",
"enableDescription": "도우미가 사용자의 설정을 읽고 승인할 변경 사항을 제안하도록 하세요.",
"readOnlyCommands": "읽기 전용 진단 명령을 허용합니다.",
"readOnlyCommandsDescription": "도우미는 매번 묻지 않고 df 및 uptime과 같은 안전한 명령어 목록을 실행할 수 있습니다. 서버를 변경하는 작업은 항상 먼저 제안됩니다.",
"modelDetectFailed": "공급업체에 연결할 수 없어서 일반적인 목록을 제공합니다. 모델명을 입력하셔도 됩니다.",
"modelCustom": "모델명을 입력하세요",
"modelRefresh": "목록 새로 고침",
"modelPlaceholder": "모델을 선택하거나 입력하세요.",
"attachHint": "호스트, 코드 조각 또는 자동화를 참조하려면 @를 입력하세요. 승인을 위해 변경 사항이 제안됩니다.",
"runCommandTitle": "명령어를 실행하세요",
"assistant": "AI 어시스턴트",
"terminalPanelEmpty": "이 호스트에 대해 담당자에게 문의하십시오. 상태가 변경되는 모든 사항은 먼저 사용자의 승인이 필요합니다.",
"terminalPanelPlaceholder": "이 호스트에 대해 문의해 보세요...",
"noProvidersConfigured": "아직 AI 제공업체가 구성되지 않았습니다. AI 도우미 탭에서 추가하세요.",
"runsInOpenTerminal": "열려 있는 터미널 세션에서 실행됩니다.",
"runInTerminal": "달리다"
},
"nav": {
"ai": "일체 포함"
},
"onboarding": {
"aiTitle": "AI 비서",
"aiIntro": "Termix에는 선택적으로 AI 비서 기능이 포함될 수 있습니다. 이 기능은 나중에 설정에서 변경할 수 있습니다.",
"aiSkipTitle": "꺼두세요",
"aiSkipDesc": "Termix는 현재와 같이 작동합니다. 어시스턴트는 숨겨진 상태로 유지됩니다.",
"aiEnableTitle": "켜세요",
"aiEnableDesc": "설정을 읽고 사용자가 승인하는 변경 사항을 제안하는 도우미를 추가합니다. 사용자가 제공하는 공급업체(예: 지역 Ollama)가 필요합니다.",
"aiNote": "공급자를 추가하고 메시지를 보내기 전까지는 아무것도 전송되지 않습니다."
},
"admin": {
"aiGloballyEnabled": "AI 비서",
"aiGloballyEnabledDesc": "사용자가 AI 비서를 켜고 끌 수 있도록 하세요. AI 비서가 꺼져 있으면 모든 사용자에게 비서 기능이 숨겨지고 차단됩니다.",
"aiPrivateEndpoints": "개인 AI 호스트 허용",
"aiPrivateEndpointsDesc": "사용자가 공급자에게 연결할 수 있는 개인 네트워크의 호스트(예: 자체 호스팅 Ollama)를 나열하세요. 호스트는 쉼표로 구분하세요.",
"updateAiEnabledFailed": "AI 설정을 업데이트할 수 없습니다.",
"updateAiEndpointsFailed": "허용된 호스트를 업데이트할 수 없습니다."
}
}
+78
View File
@@ -0,0 +1,78 @@
{
"ai": {
"title": "Assistent",
"send": "Versturen",
"stop": "Stop",
"save": "Redden",
"cancel": "Annuleren",
"running": "rennen",
"approve": "Goedkeuren",
"reject": "Afwijzen",
"inputPlaceholder": "Vraag naar uw servers of beschrijf een voorgestelde wijziging.",
"selectProvider": "Selecteer een aanbieder",
"providerSettings": "Aanbiederinstellingen",
"addProvider": "Aanbieder toevoegen",
"editProvider": "Bewerk provider",
"removeProvider": "Verwijder provider",
"providerType": "Aanbieder",
"providerLabel": "Naam",
"providerLabelPlaceholder": "Bijvoorbeeld: Lokale Ollama",
"baseUrl": "Serveradres",
"apiKey": "API-sleutel",
"defaultModel": "Standaardmodel",
"defaultModelPlaceholder": "Bijvoorbeeld: llama3.1 of claude-opus-5",
"labelRequired": "Geef deze aanbieder een naam",
"providerSaveFailed": "De provider kon niet worden opgeslagen.",
"providerUpdated": "Aanbieder bijgewerkt",
"providerDeleteFailed": "De provider kon niet worden verwijderd.",
"privateEndpointHint": "IP-adressen op uw privénetwerk moeten eerst door een beheerder worden toegestaan.",
"providerOllama": "Ollama",
"providerAnthropic": "Antropisch",
"providerOpenai": "OpenAI",
"providerGemini": "Google Gemini",
"providerOpenaiCompatible": "OpenAI-compatibel",
"toolArguments": "Invoer",
"toolResult": "Resultaat",
"statusApplied": "Toegepast",
"statusRejected": "Afgewezen",
"proposalApplied": "Toegepaste wijziging",
"proposalApplyFailed": "De wijziging kon niet worden toegepast.",
"proposalRejectFailed": "Kon de wijziging niet afwijzen",
"enableTitle": "Schakel de AI-assistent in",
"enableDescription": "Laat de assistent je instellingen lezen en wijzigingen voorstellen die je vervolgens kunt goedkeuren.",
"readOnlyCommands": "Sta alleen-lezen diagnostische opdrachten toe.",
"readOnlyCommandsDescription": "De assistent kan een korte lijst met veilige commando's zoals df en uptime uitvoeren zonder telkens opnieuw toestemming te vragen. Alles wat een server wijzigt, wordt altijd eerst voorgesteld.",
"modelDetectFailed": "De aanbieder kon niet worden bereikt, daarom is dit een algemene lijst. U kunt elke modelnaam invoeren.",
"modelCustom": "Voer een modelnaam in",
"modelRefresh": "Vernieuw de lijst",
"modelPlaceholder": "Kies of typ een model",
"attachHint": "Typ @ om te verwijzen naar een host, codefragment of automatisering. Wijzigingen worden voorgesteld ter goedkeuring.",
"runCommandTitle": "Voer het commando uit",
"assistant": "AI-assistent",
"terminalPanelEmpty": "Vraag de assistent naar deze host. Alles wat de status wijzigt, vereist eerst uw goedkeuring.",
"terminalPanelPlaceholder": "Vraag naar deze host...",
"noProvidersConfigured": "Er is nog geen AI-provider geconfigureerd. Voeg er een toe via het tabblad AI-assistent.",
"runsInOpenTerminal": "Wordt uitgevoerd in uw geopende terminalsessie.",
"runInTerminal": "Loop"
},
"nav": {
"ai": "AI"
},
"onboarding": {
"aiTitle": "AI-assistent",
"aiIntro": "Termix kan optioneel een AI-assistent bevatten. Je kunt dit later in je instellingen wijzigen.",
"aiSkipTitle": "Laat het weg",
"aiSkipDesc": "Termix werkt zoals het nu werkt. De assistent blijft verborgen.",
"aiEnableTitle": "Schakel het in.",
"aiEnableDesc": "Voegt een assistent toe die je instellingen leest en wijzigingen voorstelt die je kunt goedkeuren. Vereist een leverancier die je zelf aanlevert, zoals een lokale Ollama.",
"aiNote": "Er wordt niets verzonden totdat je een provider toevoegt en een bericht verstuurt."
},
"admin": {
"aiGloballyEnabled": "AI-assistent",
"aiGloballyEnabledDesc": "Gebruikers kunnen de AI-assistent inschakelen. Wanneer deze is uitgeschakeld, is de assistent verborgen en voor iedereen geblokkeerd.",
"aiPrivateEndpoints": "Toegestane privé-AI-hosts",
"aiPrivateEndpointsDesc": "Hosts op uw privénetwerk waarnaar gebruikers een provider kunnen verwijzen, zoals een zelfgehoste Ollama. Scheid ze met komma's.",
"updateAiEnabledFailed": "De AI-instelling kon niet worden bijgewerkt.",
"updateAiEndpointsFailed": "Het lukte niet om de lijst met toegestane hosts bij te werken."
}
}
+78
View File
@@ -0,0 +1,78 @@
{
"ai": {
"title": "Assistent",
"send": "Sende",
"stop": "Stoppe",
"save": "Spare",
"cancel": "Kansellere",
"running": "løping",
"approve": "Vedta",
"reject": "Avvis",
"inputPlaceholder": "Spør om servitørene dine, eller beskriv en endring du vil foreslå",
"selectProvider": "Velg en leverandør",
"providerSettings": "Leverandørinnstillinger",
"addProvider": "Legg til leverandør",
"editProvider": "Rediger leverandør",
"removeProvider": "Fjern leverandør",
"providerType": "Leverandør",
"providerLabel": "Navn",
"providerLabelPlaceholder": "For eksempel: Lokal Ollama",
"baseUrl": "Serveradresse",
"apiKey": "API-nøkkel",
"defaultModel": "Standardmodell",
"defaultModelPlaceholder": "For eksempel: llama3.1 eller claude-opus-5",
"labelRequired": "Gi denne leverandøren et navn",
"providerSaveFailed": "Kunne ikke lagre leverandøren",
"providerUpdated": "Leverandøren er oppdatert",
"providerDeleteFailed": "Kunne ikke fjerne leverandøren",
"privateEndpointHint": "Adresser på ditt private nettverk krever at en administrator tillater verten først.",
"providerOllama": "Ollama",
"providerAnthropic": "Antropisk",
"providerOpenai": "Åpen kunstig intelligens",
"providerGemini": "Google Gemini",
"providerOpenaiCompatible": "OpenAI-kompatibel",
"toolArguments": "Inndata",
"toolResult": "Resultat",
"statusApplied": "Anvendt",
"statusRejected": "Avvist",
"proposalApplied": "Endringen er brukt",
"proposalApplyFailed": "Kunne ikke bruke endringen",
"proposalRejectFailed": "Kunne ikke avvise endringen",
"enableTitle": "Aktiver AI-assistenten",
"enableDescription": "La assistenten lese oppsettet ditt og foreslå endringer du kan godkjenne.",
"readOnlyCommands": "Tillat skrivebeskyttede diagnostiske kommandoer",
"readOnlyCommandsDescription": "Assistenten kan kjøre en kort liste med sikre kommandoer som df og uptime uten å spørre hver gang. Alt som endrer en server blir alltid foreslått først.",
"modelDetectFailed": "Kunne ikke nå leverandøren, så dette er en generell liste. Du kan skrive inn et hvilket som helst modellnavn.",
"modelCustom": "Skriv inn et modellnavn",
"modelRefresh": "Oppdater listen",
"modelPlaceholder": "Velg eller skriv inn en modell",
"attachHint": "Skriv inn @ for å referere til en vert, et kodeutdrag eller en automatisering. Endringer foreslås som du kan godkjenne.",
"runCommandTitle": "Kjør kommandoen",
"assistant": "AI-assistent",
"terminalPanelEmpty": "Spør assistenten om denne verten. Alt som endrer status trenger din godkjenning først.",
"terminalPanelPlaceholder": "Spør om denne verten...",
"noProvidersConfigured": "Ingen AI-leverandør er konfigurert ennå. Legg til en i AI-assistent-fanen.",
"runsInOpenTerminal": "Kjører i din åpne terminaløkt.",
"runInTerminal": "Løp"
},
"nav": {
"ai": "AI"
},
"onboarding": {
"aiTitle": "AI-assistent",
"aiIntro": "Termix kan inkludere en valgfri AI-assistent. Du kan endre dette senere i innstillingene dine.",
"aiSkipTitle": "La det være av",
"aiSkipDesc": "Termix fungerer som den gjør nå. Assistenten forblir skjult.",
"aiEnableTitle": "Slå den på",
"aiEnableDesc": "Legger til en assistent som leser oppsettet ditt og foreslår endringer du godkjenner. Krever en leverandør du oppgir, for eksempel en lokal Ollama.",
"aiNote": "Ingenting sendes noe sted før du legger til en leverandør og sender en melding."
},
"admin": {
"aiGloballyEnabled": "AI-assistent",
"aiGloballyEnabledDesc": "La brukere slå på AI-assistenten. Mens dette er av, er assistenten skjult og blokkert for alle.",
"aiPrivateEndpoints": "Tillatte private AI-verter",
"aiPrivateEndpointsDesc": "Verter på ditt private nettverk som brukere kan peke en leverandør til, for eksempel en selvhostet Ollama. Skill dem med komma.",
"updateAiEnabledFailed": "Kunne ikke oppdatere AI-innstillingen",
"updateAiEndpointsFailed": "Kunne ikke oppdatere de tillatte vertene"
}
}
+78
View File
@@ -0,0 +1,78 @@
{
"ai": {
"title": "Asystent",
"send": "Wyślij",
"stop": "Zatrzymaj",
"save": "Zapisz",
"cancel": "Anuluj",
"running": "działa",
"approve": "Zatwierdź",
"reject": "Odrzuć",
"inputPlaceholder": "Zapytaj o swoje serwery lub opisz proponowaną zmianę",
"selectProvider": "Wybierz dostawcę",
"providerSettings": "Ustawienia dostawcy",
"addProvider": "Dodaj dostawcę",
"editProvider": "Edytuj dostawcę",
"removeProvider": "Usuń dostawcę",
"providerType": "Dostawca",
"providerLabel": "Nazwa",
"providerLabelPlaceholder": "Na przykład: Ollama lokalnie",
"baseUrl": "Adres serwera",
"apiKey": "Klucz API",
"defaultModel": "Model domyślny",
"defaultModelPlaceholder": "Na przykład: llama3.1 lub claude-opus-5",
"labelRequired": "Podaj nazwę tego dostawcy",
"providerSaveFailed": "Nie udało się zapisać dostawcy",
"providerUpdated": "Zaktualizowano dostawcę",
"providerDeleteFailed": "Nie można usunąć dostawcy",
"privateEndpointHint": "Adresy w sieci prywatnej wymagają, aby administrator najpierw dopuścił dany host.",
"providerOllama": "Ollama",
"providerAnthropic": "Anthropic",
"providerOpenai": "OpenAI",
"providerGemini": "Google Gemini",
"providerOpenaiCompatible": "Kompatybilny z OpenAI",
"toolArguments": "Dane wejściowe",
"toolResult": "Wynik",
"statusApplied": "Zastosowano",
"statusRejected": "Odrzucono",
"proposalApplied": "Zastosowano zmianę",
"proposalApplyFailed": "Nie można zastosować zmiany",
"proposalRejectFailed": "Nie można odrzucić zmiany",
"enableTitle": "Włącz asystenta AI",
"enableDescription": "Pozwól asystentowi odczytać konfigurację i proponować zmiany do Twojego zatwierdzenia.",
"readOnlyCommands": "Zezwalaj na polecenia diagnostyczne tylko do odczytu",
"readOnlyCommandsDescription": "Asystent może uruchamiać krótką listę bezpiecznych poleceń, takich jak df czy uptime, bez pytania za każdym razem. Wszystko, co zmienia serwer, jest zawsze najpierw proponowane do zatwierdzenia.",
"modelDetectFailed": "Nie udało się skontaktować z dostawcą, więc jest to lista ogólna. Możesz wpisać dowolną nazwę modelu.",
"modelCustom": "Wprowadź nazwę modelu",
"modelRefresh": "Odśwież listę",
"modelPlaceholder": "Wybierz lub wpisz model",
"attachHint": "Wpisz @, aby odwołać się do hosta, snippetu lub automatyzacji. Zmiany są proponowane do Twojego zatwierdzenia.",
"runCommandTitle": "Uruchom polecenie",
"assistant": "Asystent AI",
"terminalPanelEmpty": "Zapytaj asystenta o tego hosta. Każda zmiana stanu wymaga Twojej zgody.",
"terminalPanelPlaceholder": "Zapytaj o tego gospodarza...",
"noProvidersConfigured": "Nie skonfigurowano jeszcze żadnego dostawcy AI. Dodaj go w zakładce Asystent AI.",
"runsInOpenTerminal": "Działa w otwartej sesji terminalowej.",
"runInTerminal": "Uruchomić"
},
"nav": {
"ai": "AI"
},
"onboarding": {
"aiTitle": "Asystent AI",
"aiIntro": "Termix może zawierać opcjonalnego asystenta AI. Możesz to później zmienić w ustawieniach.",
"aiSkipTitle": "Zostaw wyłączone",
"aiSkipDesc": "Termix działa tak jak teraz. Asystent pozostaje ukryty.",
"aiEnableTitle": "Włącz",
"aiEnableDesc": "Dodaje asystenta, który odczytuje Twoją konfigurację i sugeruje zmiany, które zatwierdzasz. Wymaga wskazanego przez Ciebie dostawcy, np. lokalnej Ollamy.",
"aiNote": "Dopóki nie dodasz dostawcy i nie wyślesz wiadomości, nic nie zostanie wysłane."
},
"admin": {
"aiGloballyEnabled": "Asystent AI",
"aiGloballyEnabledDesc": "Pozwól użytkownikom włączyć asystenta AI. Gdy ta opcja jest wyłączona, asystent jest ukryty i zablokowany dla wszystkich.",
"aiPrivateEndpoints": "Dozwolone prywatne hosty AI",
"aiPrivateEndpointsDesc": "Hosty w Twojej sieci prywatnej, na które użytkownicy mogą wskazać dostawcę, na przykład własna instancja Ollamy. Rozdziel je przecinkami.",
"updateAiEnabledFailed": "Nie można zaktualizować ustawień AI",
"updateAiEndpointsFailed": "Nie można zaktualizować dozwolonych hostów"
}
}
+78
View File
@@ -0,0 +1,78 @@
{
"ai": {
"title": "Assistente",
"send": "Enviar",
"stop": "Parar",
"save": "Salvar",
"cancel": "Cancelar",
"running": "correndo",
"approve": "Aprovar",
"reject": "Rejeitar",
"inputPlaceholder": "Pergunte sobre seus servidores ou descreva uma alteração que você gostaria de propor.",
"selectProvider": "Selecione um fornecedor",
"providerSettings": "Configurações do provedor",
"addProvider": "Adicionar provedor",
"editProvider": "Editar provedor",
"removeProvider": "Remover provedor",
"providerType": "Fornecedor",
"providerLabel": "Nome",
"providerLabelPlaceholder": "Por exemplo: Ollama local",
"baseUrl": "Endereço do servidor",
"apiKey": "Chave de API",
"defaultModel": "Modelo padrão",
"defaultModelPlaceholder": "Por exemplo: llama3.1 ou claude-opus-5",
"labelRequired": "Dê um nome a este fornecedor.",
"providerSaveFailed": "Não foi possível salvar o provedor.",
"providerUpdated": "Fornecedor atualizado",
"providerDeleteFailed": "Não foi possível remover o provedor.",
"privateEndpointHint": "Os endereços na sua rede privada precisam que um administrador autorize o host primeiro.",
"providerOllama": "Ollama",
"providerAnthropic": "Antrópico",
"providerOpenai": "OpenAI",
"providerGemini": "Google Gemini",
"providerOpenaiCompatible": "Compatível com OpenAI",
"toolArguments": "Entrada",
"toolResult": "Resultado",
"statusApplied": "Aplicado",
"statusRejected": "Rejeitado",
"proposalApplied": "Alteração aplicada",
"proposalApplyFailed": "Não foi possível aplicar a alteração.",
"proposalRejectFailed": "Não foi possível rejeitar a alteração.",
"enableTitle": "Ative o assistente de IA",
"enableDescription": "Deixe o assistente ler sua configuração e sugerir alterações para você aprovar.",
"readOnlyCommands": "Permitir comandos de diagnóstico somente leitura",
"readOnlyCommandsDescription": "O assistente pode executar uma pequena lista de comandos seguros, como `df` e `uptime`, sem perguntar a cada vez. Qualquer alteração que faça no servidor será sempre sugerida primeiro.",
"modelDetectFailed": "Não foi possível contatar o fornecedor, portanto esta é uma lista genérica. Você pode digitar o nome de qualquer modelo.",
"modelCustom": "Insira o nome do modelo",
"modelRefresh": "Atualizar lista",
"modelPlaceholder": "Selecione ou digite um modelo",
"attachHint": "Digite @ para fazer referência a um host, snippet ou automação. As alterações são propostas para sua aprovação.",
"runCommandTitle": "Executar comando",
"assistant": "Assistente de IA",
"terminalPanelEmpty": "Pergunte ao assistente sobre este anfitrião. Qualquer alteração de estado precisa da sua aprovação prévia.",
"terminalPanelPlaceholder": "Pergunte sobre este anfitrião...",
"noProvidersConfigured": "Nenhum provedor de IA está configurado ainda. Adicione um na aba do assistente de IA.",
"runsInOpenTerminal": "Executa na sua sessão de terminal aberta.",
"runInTerminal": "Correr"
},
"nav": {
"ai": "IA"
},
"onboarding": {
"aiTitle": "assistente de IA",
"aiIntro": "O Termix pode incluir um assistente de IA opcional. Você pode alterar isso posteriormente nas suas configurações.",
"aiSkipTitle": "Deixe desligado",
"aiSkipDesc": "O Termix funciona como está agora. O assistente permanece oculto.",
"aiEnableTitle": "Ligue-o",
"aiEnableDesc": "Adiciona um assistente que lê sua configuração e sugere alterações que você aprova. Requer um provedor fornecido por você, como um Ollama local.",
"aiNote": "Nada será enviado até que você adicione um provedor e envie uma mensagem."
},
"admin": {
"aiGloballyEnabled": "assistente de IA",
"aiGloballyEnabledDesc": "Permita que os usuários ativem o assistente de IA. Quando estiver desativado, o assistente ficará oculto e bloqueado para todos.",
"aiPrivateEndpoints": "Hosts de IA privados permitidos",
"aiPrivateEndpointsDesc": "Hosts em sua rede privada que os usuários podem usar como provedores, como um Ollama auto-hospedado. Separe-os com vírgulas.",
"updateAiEnabledFailed": "Não foi possível atualizar as configurações de IA.",
"updateAiEndpointsFailed": "Não foi possível atualizar os hosts permitidos."
}
}
+78
View File
@@ -0,0 +1,78 @@
{
"ai": {
"title": "Assistente",
"send": "Enviar",
"stop": "Parar",
"save": "Guardar",
"cancel": "Cancelar",
"running": "corrida",
"approve": "Aprovar",
"reject": "Rejeitar",
"inputPlaceholder": "Pergunte pelos seus servidores ou descreva uma alteração que gostaria de propor.",
"selectProvider": "Selecione um fornecedor",
"providerSettings": "Configurações do fornecedor",
"addProvider": "Adicionar provedor",
"editProvider": "Editar provedor",
"removeProvider": "Remover fornecedor",
"providerType": "Fornecedor",
"providerLabel": "Nome",
"providerLabelPlaceholder": "Por exemplo: Ollama local",
"baseUrl": "Endereço do servidor",
"apiKey": "Chave API",
"defaultModel": "Modelo padrão",
"defaultModelPlaceholder": "Por exemplo: llama3.1 ou claude-opus-5",
"labelRequired": "Dê um nome a este fornecedor.",
"providerSaveFailed": "Não foi possível guardar o fornecedor.",
"providerUpdated": "Fornecedor atualizado",
"providerDeleteFailed": "Não foi possível remover o fornecedor.",
"privateEndpointHint": "Os endereços na sua rede privada necessitam que um administrador autorize primeiro o host.",
"providerOllama": "Ollama",
"providerAnthropic": "Antrópico",
"providerOpenai": "OpenAI",
"providerGemini": "Google Gemini",
"providerOpenaiCompatible": "Compatível com OpenAI",
"toolArguments": "Entrada",
"toolResult": "Resultado",
"statusApplied": "Aplicado",
"statusRejected": "Rejeitado",
"proposalApplied": "Alteração aplicada",
"proposalApplyFailed": "Não foi possível aplicar a alteração.",
"proposalRejectFailed": "Não foi possível rejeitar a alteração.",
"enableTitle": "Ative o assistente de IA",
"enableDescription": "Deixe o assistente ler a sua configuração e sugerir alterações para que a aprove.",
"readOnlyCommands": "Permitir comandos de diagnóstico de leitura apenas",
"readOnlyCommandsDescription": "O assistente pode executar uma pequena lista de comandos seguros, como `df` e `uptime`, sem perguntar de cada vez. Qualquer alteração que faça no servidor será sempre sugerida em primeiro lugar.",
"modelDetectFailed": "Não foi possível contactar o fornecedor, pelo que esta é uma lista genérica. Pode digitar o nome de qualquer modelo.",
"modelCustom": "Introduza o nome do modelo",
"modelRefresh": "Atualizar lista",
"modelPlaceholder": "Selecione ou introduza um modelo",
"attachHint": "Digite @ para fazer referência a um host, snippet ou automação. As alterações são propostas para a sua aprovação.",
"runCommandTitle": "Executar comando",
"assistant": "Assistente de IA",
"terminalPanelEmpty": "Pergunte ao assistente sobre este anfitrião. Qualquer alteração de estado necessita da sua aprovação prévia.",
"terminalPanelPlaceholder": "Pergunte sobre este anfitrião...",
"noProvidersConfigured": "Nenhum fornecedor de IA está configurado ainda. Adicione um no separador do assistente de IA.",
"runsInOpenTerminal": "Executa na sua sessão de terminal aberta.",
"runInTerminal": "Correr"
},
"nav": {
"ai": "IA"
},
"onboarding": {
"aiTitle": "assistente de IA",
"aiIntro": "O Termix pode incluir um assistente de IA opcional. Pode alterar isso posteriormente nas suas definições.",
"aiSkipTitle": "Deixe desligado",
"aiSkipDesc": "O Termix funciona como está agora. O assistente permanece oculto.",
"aiEnableTitle": "Ligue-o",
"aiEnableDesc": "Adiciona um assistente que lê a sua configuração e sugere alterações que aprova. Requer um fornecedor fornecido por si, como um Ollama local.",
"aiNote": "Nada será enviado até que adicione um fornecedor e envie uma mensagem."
},
"admin": {
"aiGloballyEnabled": "assistente de IA",
"aiGloballyEnabledDesc": "Permita que os utilizadores ativem o assistente de IA. Quando estiver desativado, o assistente ficará oculto e bloqueado para todos.",
"aiPrivateEndpoints": "Hosts de IA privados permitidos",
"aiPrivateEndpointsDesc": "Hosts na sua rede privada que os utilizadores podem utilizar como fornecedores, como um Ollama auto-hospedado. Separe-os com vírgulas.",
"updateAiEnabledFailed": "Não foi possível atualizar as definições de IA.",
"updateAiEndpointsFailed": "Não foi possível atualizar os hosts permitidos."
}
}
+78
View File
@@ -0,0 +1,78 @@
{
"ai": {
"title": "Asistent",
"send": "Trimite",
"stop": "Stop",
"save": "Salva",
"cancel": "Anula",
"running": "funcţionare",
"approve": "Aproba",
"reject": "Respinge",
"inputPlaceholder": "Întreabă despre chelnerii tăi sau descrie o schimbare pe care o propui.",
"selectProvider": "Selectați un furnizor",
"providerSettings": "Setări furnizor",
"addProvider": "Adăugați furnizor",
"editProvider": "Editați furnizorul",
"removeProvider": "Eliminați furnizorul",
"providerType": "Furnizor",
"providerLabel": "Nume",
"providerLabelPlaceholder": "De exemplu: Ollama locală",
"baseUrl": "Adresa serverului",
"apiKey": "Cheie API",
"defaultModel": "Model implicit",
"defaultModelPlaceholder": "De exemplu: llama3.1 sau claude-opus-5",
"labelRequired": "Dați un nume acestui furnizor",
"providerSaveFailed": "Nu s-a putut salva furnizorul",
"providerUpdated": "Furnizor actualizat",
"providerDeleteFailed": "Nu s-a putut elimina furnizorul",
"privateEndpointHint": "Adresele din rețeaua privată necesită mai întâi permisiunea unui administrator pentru gazdă.",
"providerOllama": "Ollama",
"providerAnthropic": "Antropic",
"providerOpenai": "OpenAI",
"providerGemini": "Google Gemeni",
"providerOpenaiCompatible": "Compatibil cu OpenAI",
"toolArguments": "Intrare",
"toolResult": "Rezultat",
"statusApplied": "Aplicat",
"statusRejected": "Respins",
"proposalApplied": "Modificare aplicată",
"proposalApplyFailed": "Nu s-a putut aplica modificarea",
"proposalRejectFailed": "Nu s-a putut respinge modificarea",
"enableTitle": "Activează asistentul AI",
"enableDescription": "Permiteți asistentului să citească configurația și să sugereze modificări pentru aprobare.",
"readOnlyCommands": "Permite comenzi de diagnosticare doar în citire",
"readOnlyCommandsDescription": "Asistentul poate rula o listă scurtă de comenzi sigure, cum ar fi df și uptime, fără a întreba de fiecare dată. Orice modificare a unui server este întotdeauna propusă prima.",
"modelDetectFailed": "Nu s-a putut contacta furnizorul, așadar aceasta este o listă generală. Puteți introduce orice nume de model.",
"modelCustom": "Introduceți un nume de model",
"modelRefresh": "Reîmprospătați lista",
"modelPlaceholder": "Alegeți sau introduceți un model",
"attachHint": "Tastați @ pentru a face referire la o gazdă, un fragment de cod sau o automatizare. Sunt propuse modificări pentru aprobare.",
"runCommandTitle": "Comandă de rulare",
"assistant": "Asistent AI",
"terminalPanelEmpty": "Întreabă asistentul despre această gazdă. Orice modificare a stării necesită mai întâi aprobarea ta.",
"terminalPanelPlaceholder": "Întreabă despre această gazdă...",
"noProvidersConfigured": "Încă nu este configurat niciun furnizor de inteligență artificială. Adăugați unul în fila Asistent de inteligență artificială.",
"runsInOpenTerminal": "Rulează în sesiunea de terminal deschisă.",
"runInTerminal": "Aleargă"
},
"nav": {
"ai": "IA"
},
"onboarding": {
"aiTitle": "Asistent AI",
"aiIntro": "Termix poate include un asistent AI opțional. Puteți modifica acest lucru ulterior în setări.",
"aiSkipTitle": "Lasă-l deoparte",
"aiSkipDesc": "Termix funcționează ca acum. Asistentul rămâne ascuns.",
"aiEnableTitle": "Pornește-l",
"aiEnableDesc": "Adaugă un asistent care citește configurația și sugerează modificări pe care le aprobi. Necesită un furnizor furnizat de tine, cum ar fi un Ollama local.",
"aiNote": "Nimic nu este trimis nicăieri până nu adăugați un furnizor și nu trimiteți un mesaj."
},
"admin": {
"aiGloballyEnabled": "Asistent AI",
"aiGloballyEnabledDesc": "Permiteți utilizatorilor să activeze asistentul AI. Cât timp este dezactivat, asistentul este ascuns și blocat pentru toată lumea.",
"aiPrivateEndpoints": "Gazde private AI permise",
"aiPrivateEndpointsDesc": "Gazde din rețeaua privată către care utilizatorii pot direcționa un furnizor, cum ar fi un Ollama auto-găzduit. Separați-le prin virgule.",
"updateAiEnabledFailed": "Nu s-a putut actualiza setarea AI",
"updateAiEndpointsFailed": "Nu s-au putut actualiza gazdele permise"
}
}
+78
View File
@@ -0,0 +1,78 @@
{
"ai": {
"title": "Помощник",
"send": "Отправлять",
"stop": "Останавливаться",
"save": "Сохранять",
"cancel": "Отмена",
"running": "бег",
"approve": "Утвердить",
"reject": "Отклонять",
"inputPlaceholder": "Поинтересуйтесь состоянием ваших серверов или предложите внести изменения.",
"selectProvider": "Выберите поставщика",
"providerSettings": "Настройки поставщика",
"addProvider": "Добавить поставщика",
"editProvider": "Поставщик редактирования",
"removeProvider": "Удалить поставщика",
"providerType": "Поставщик",
"providerLabel": "Имя",
"providerLabelPlaceholder": "Например: Местная лама",
"baseUrl": "Адрес сервера",
"apiKey": "ключ API",
"defaultModel": "Модель по умолчанию",
"defaultModelPlaceholder": "Например: llama3.1 или claude-opus-5",
"labelRequired": "Укажите имя этого поставщика услуг.",
"providerSaveFailed": "Не удалось сохранить поставщика услуг.",
"providerUpdated": "Поставщик обновлен",
"providerDeleteFailed": "Не удалось удалить поставщика услуг.",
"privateEndpointHint": "Для доступа к адресам в вашей частной сети необходимо предварительное разрешение администратора.",
"providerOllama": "оллама",
"providerAnthropic": "Антропический",
"providerOpenai": "OpenAI",
"providerGemini": "Google Gemini",
"providerOpenaiCompatible": "Совместимость с OpenAI",
"toolArguments": "Вход",
"toolResult": "Результат",
"statusApplied": "Применяемый",
"statusRejected": "Отклоненный",
"proposalApplied": "Внесены изменения.",
"proposalApplyFailed": "Не удалось применить изменения.",
"proposalRejectFailed": "Не мог отклонить изменение.",
"enableTitle": "Включите ИИ-помощника",
"enableDescription": "Позвольте ассистенту ознакомиться с вашими настройками и предложить изменения для утверждения.",
"readOnlyCommands": "Разрешить диагностические команды только для чтения",
"readOnlyCommandsDescription": "Ассистент может выполнить короткий список безопасных команд, таких как df и uptime, не запрашивая подтверждения каждый раз. Любая команда, изменяющая состояние сервера, всегда предлагается в первую очередь.",
"modelDetectFailed": "Не удалось связаться с поставщиком, поэтому это общий список. Вы можете ввести любое название модели.",
"modelCustom": "Введите название модели",
"modelRefresh": "Обновить список",
"modelPlaceholder": "Выберите или введите модель",
"attachHint": "Введите @ для ссылки на хост, фрагмент кода или автоматизацию. Предлагаются изменения для вашего утверждения.",
"runCommandTitle": "Выполните команду",
"assistant": "ИИ-помощник",
"terminalPanelEmpty": "Спросите помощника об этом хосте. Любое изменение состояния требует вашего предварительного одобрения.",
"terminalPanelPlaceholder": "Спросите об этом хостинге...",
"noProvidersConfigured": "Поставщик ИИ пока не настроен. Добавьте его на вкладке «Помощник ИИ».",
"runsInOpenTerminal": "Запускается в открытой терминальной сессии.",
"runInTerminal": "Бегать"
},
"nav": {
"ai": "ИИ"
},
"onboarding": {
"aiTitle": "ИИ-помощник",
"aiIntro": "В Termix можно добавить опционального ИИ-помощника. Это можно изменить позже в настройках.",
"aiSkipTitle": "Оставьте это выключенным.",
"aiSkipDesc": "Termix работает так, как сейчас. Ассистент остается скрытым.",
"aiEnableTitle": "Включите его",
"aiEnableDesc": "Добавляет помощника, который считывает ваши настройки и предлагает изменения, которые вы одобряете. Требуется поставщик, которого вы предоставляете, например, локальный Ollama.",
"aiNote": "Ничего никуда не отправляется, пока вы не добавите поставщика услуг и не отправите сообщение."
},
"admin": {
"aiGloballyEnabled": "ИИ-помощник",
"aiGloballyEnabledDesc": "Разрешите пользователям включить голосового помощника. Если он выключен, помощник скрыт и заблокирован для всех.",
"aiPrivateEndpoints": "Разрешены частные хосты ИИ",
"aiPrivateEndpointsDesc": "Укажите хосты в вашей частной сети, на которые пользователи могут направлять запросы провайдера, например, самостоятельно размещенный Ollama. Разделяйте их запятыми.",
"updateAiEnabledFailed": "Не удалось обновить настройки ИИ.",
"updateAiEndpointsFailed": "Не удалось обновить список разрешенных хостов."
}
}
+78
View File
@@ -0,0 +1,78 @@
{
"ai": {
"title": "Асистент",
"send": "Пошаљи",
"stop": "Заустави",
"save": "Сачувај",
"cancel": "Откажи",
"running": "трчање",
"approve": "Одобри",
"reject": "Одбиј",
"inputPlaceholder": "Питајте о својим серверима или опишите промену коју желите да предложите",
"selectProvider": "Изаберите добављача",
"providerSettings": "Подешавања добављача",
"addProvider": "Додај добављача",
"editProvider": "Измени добављача",
"removeProvider": "Уклони добављача",
"providerType": "Провајдер",
"providerLabel": "Име",
"providerLabelPlaceholder": "На пример: Локална Олама",
"baseUrl": "Адреса сервера",
"apiKey": "API кључ",
"defaultModel": "Подразумевани модел",
"defaultModelPlaceholder": "На пример: llama3.1 или claude-opus-5",
"labelRequired": "Дајте име овом добављачу",
"providerSaveFailed": "Није могуће сачувати добављача",
"providerUpdated": "Провајдер је ажуриран",
"providerDeleteFailed": "Није могуће уклонити добављача",
"privateEndpointHint": "Адресе на вашој приватној мрежи прво треба да дозволе администратору хоста.",
"providerOllama": "Олама",
"providerAnthropic": "Антропски",
"providerOpenai": "OpenAI",
"providerGemini": "Гугл Џемини",
"providerOpenaiCompatible": "Компатибилно са OpenAI-ом",
"toolArguments": "Унос",
"toolResult": "Резултат",
"statusApplied": "Примењено",
"statusRejected": "Одбијено",
"proposalApplied": "Промена је примењена",
"proposalApplyFailed": "Није могуће применити промену",
"proposalRejectFailed": "Није могуће одбити промену",
"enableTitle": "Омогућите вештачку интелигенцију (AI) асистента",
"enableDescription": "Нека асистент прочита ваша подешавања и предложи измене које ћете одобрити.",
"readOnlyCommands": "Дозволи дијагностичке команде само за читање",
"readOnlyCommandsDescription": "Асистент може да покрене кратку листу безбедних команди као што су df и uptime без сваког питања. Све што мења сервер се увек прво предлаже.",
"modelDetectFailed": "Нисмо могли да контактирамо добављача, тако да је ово општа листа. Можете да унесете било који назив модела.",
"modelCustom": "Унесите назив модела",
"modelRefresh": "Освежи листу",
"modelPlaceholder": "Изаберите или унесите модел",
"attachHint": "Унесите @ да бисте референцирали хост, исечак кода или аутоматизацију. Промене се предлажу да бисте их одобрили.",
"runCommandTitle": "Покрени команду",
"assistant": "АИ асистент",
"terminalPanelEmpty": "Питајте асистента о овом домаћину. Све што мења стање прво захтева ваше одобрење.",
"terminalPanelPlaceholder": "Питајте о овом домаћину...",
"noProvidersConfigured": "Још увек није конфигурисан ниједан добављач вештачке интелигенције. Додајте га на картици помоћника за вештачку интелигенцију.",
"runsInOpenTerminal": "Покреће се у вашој отвореној терминалној сесији.",
"runInTerminal": "Трчи"
},
"nav": {
"ai": "Вештачка интелигенција"
},
"onboarding": {
"aiTitle": "Асистент за вештачку интелигенцију",
"aiIntro": "Termix може да укључи опционог AI асистента. Ово можете касније променити у подешавањима.",
"aiSkipTitle": "Остави то искључено",
"aiSkipDesc": "Термикс ради као и сада. Асистент остаје скривен.",
"aiEnableTitle": "Укључи га",
"aiEnableDesc": "Додаје асистента који чита ваша подешавања и предлаже измене које одобравате. Потребан је добављач кога ви обезбедите, као што је локални Олама.",
"aiNote": "Ништа се не шаље нигде док не додате провајдера и не пошаљете поруку."
},
"admin": {
"aiGloballyEnabled": "Асистент за вештачку интелигенцију",
"aiGloballyEnabledDesc": "Дозволите корисницима да укључе вештачку интелигенцију асистента. Док је ово искључено, асистент је скривен и блокиран за све.",
"aiPrivateEndpoints": "Дозвољени приватни AI хостови",
"aiPrivateEndpointsDesc": "Домаћини на вашој приватној мрежи на које корисници могу усмерити провајдера, као што је самостално хостована Ollama. Раздвојите их зарезима.",
"updateAiEnabledFailed": "Није могуће ажурирати подешавање вештачке интелигенције",
"updateAiEndpointsFailed": "Није могуће ажурирати дозвољене хостове"
}
}
+78
View File
@@ -0,0 +1,78 @@
{
"ai": {
"title": "Assistent",
"send": "Skicka",
"stop": "Stopp",
"save": "Spara",
"cancel": "Avboka",
"running": "spring",
"approve": "Godkänna",
"reject": "Avvisa",
"inputPlaceholder": "Fråga om era servrar, eller beskriv en förändring att föreslå",
"selectProvider": "Välj en leverantör",
"providerSettings": "Leverantörsinställningar",
"addProvider": "Lägg till leverantör",
"editProvider": "Redigera leverantör",
"removeProvider": "Ta bort leverantör",
"providerType": "Leverantör",
"providerLabel": "Namn",
"providerLabelPlaceholder": "Till exempel: Lokal Ollama",
"baseUrl": "Serveradress",
"apiKey": "API-nyckel",
"defaultModel": "Standardmodell",
"defaultModelPlaceholder": "Till exempel: llama3.1 eller claude-opus-5",
"labelRequired": "Ge den här leverantören ett namn",
"providerSaveFailed": "Kunde inte spara leverantören",
"providerUpdated": "Leverantören uppdaterad",
"providerDeleteFailed": "Kunde inte ta bort leverantören",
"privateEndpointHint": "Adresser i ditt privata nätverk behöver en administratör som först tillåter värden.",
"providerOllama": "Ollama",
"providerAnthropic": "Antropisk",
"providerOpenai": "Öppen AI",
"providerGemini": "Google Gemini",
"providerOpenaiCompatible": "OpenAI-kompatibel",
"toolArguments": "Input",
"toolResult": "Resultat",
"statusApplied": "Tillämpad",
"statusRejected": "Avvisad",
"proposalApplied": "Ändring tillämpad",
"proposalApplyFailed": "Kunde inte tillämpa ändringen",
"proposalRejectFailed": "Kunde inte avvisa ändringen",
"enableTitle": "Aktivera AI-assistenten",
"enableDescription": "Låt assistenten läsa dina inställningar och föreslå ändringar som du kan godkänna.",
"readOnlyCommands": "Tillåt skrivskyddade diagnostikkommandon",
"readOnlyCommandsDescription": "Assistenten kan köra en kort lista med säkra kommandon som df och uptime utan att fråga varje gång. Allt som ändrar en server föreslås alltid först.",
"modelDetectFailed": "Kunde inte nå leverantören, så detta är en allmän lista. Du kan skriva in vilket modellnamn som helst.",
"modelCustom": "Ange ett modellnamn",
"modelRefresh": "Uppdatera listan",
"modelPlaceholder": "Välj eller skriv en modell",
"attachHint": "Skriv @ för att referera till en värd, ett kodavsnitt eller en automatisering. Ändringar föreslås för dig att godkänna.",
"runCommandTitle": "Kör kommando",
"assistant": "AI-assistent",
"terminalPanelEmpty": "Fråga assistenten om den här värden. Allt som ändras kräver ditt godkännande först.",
"terminalPanelPlaceholder": "Fråga om den här värden...",
"noProvidersConfigured": "Ingen AI-leverantör är konfigurerad ännu. Lägg till en på fliken AI-assistent.",
"runsInOpenTerminal": "Körs i din öppna terminalsession.",
"runInTerminal": "Sikt"
},
"nav": {
"ai": "AI"
},
"onboarding": {
"aiTitle": "AI-assistent",
"aiIntro": "Termix kan inkludera en valfri AI-assistent. Du kan ändra detta senare i dina inställningar.",
"aiSkipTitle": "Lämna den av",
"aiSkipDesc": "Termix fungerar som den gör nu. Assistenten förblir dold.",
"aiEnableTitle": "Slå på den",
"aiEnableDesc": "Lägger till en assistent som läser din installation och föreslår ändringar som du godkänner. Kräver en leverantör som du tillhandahåller, till exempel en lokal Ollama.",
"aiNote": "Ingenting skickas någonstans förrän du lägger till en leverantör och skickar ett meddelande."
},
"admin": {
"aiGloballyEnabled": "AI-assistent",
"aiGloballyEnabledDesc": "Låt användare aktivera AI-assistenten. Medan detta är avstängt är assistenten dold och blockerad för alla.",
"aiPrivateEndpoints": "Tillåtna privata AI-värdar",
"aiPrivateEndpointsDesc": "Värdar på ditt privata nätverk som användare kan hänvisa en leverantör till, till exempel en egenhostad Ollama. Separera dem med kommatecken.",
"updateAiEnabledFailed": "Kunde inte uppdatera AI-inställningen",
"updateAiEndpointsFailed": "Kunde inte uppdatera de tillåtna värdarna"
}
}
+78
View File
@@ -0,0 +1,78 @@
{
"ai": {
"title": "ผู้ช่วย",
"send": "ส่ง",
"stop": "หยุด",
"save": "บันทึก",
"cancel": "ยกเลิก",
"running": "วิ่ง",
"approve": "อนุมัติ",
"reject": "ปฏิเสธ",
"inputPlaceholder": "สอบถามเกี่ยวกับเซิร์ฟเวอร์ของคุณ หรืออธิบายการเปลี่ยนแปลงที่คุณต้องการเสนอแนะ",
"selectProvider": "เลือกผู้ให้บริการ",
"providerSettings": "การตั้งค่าผู้ให้บริการ",
"addProvider": "เพิ่มผู้ให้บริการ",
"editProvider": "แก้ไขผู้ให้บริการ",
"removeProvider": "ลบผู้ให้บริการ",
"providerType": "ผู้ให้บริการ",
"providerLabel": "ชื่อ",
"providerLabelPlaceholder": "ตัวอย่างเช่น: Ollama ท้องถิ่น",
"baseUrl": "ที่อยู่เซิร์ฟเวอร์",
"apiKey": "คีย์ API",
"defaultModel": "โมเดลเริ่มต้น",
"defaultModelPlaceholder": "ตัวอย่างเช่น: llama3.1 หรือ claude-opus-5",
"labelRequired": "ตั้งชื่อให้ผู้ให้บริการรายนี้",
"providerSaveFailed": "ไม่สามารถบันทึกผู้ให้บริการได้",
"providerUpdated": "ผู้ให้บริการได้รับการอัปเดตแล้ว",
"providerDeleteFailed": "ไม่สามารถลบผู้ให้บริการได้",
"privateEndpointHint": "การเข้าถึงที่อยู่บนเครือข่ายส่วนตัวของคุณนั้น จำเป็นต้องให้ผู้ดูแลระบบอนุญาตก่อน",
"providerOllama": "โอลาม่า",
"providerAnthropic": "แอนโทรปิก",
"providerOpenai": "โอเพ่นไอ",
"providerGemini": "กูเกิล เจมินี",
"providerOpenaiCompatible": "เข้ากันได้กับ OpenAI",
"toolArguments": "ป้อนข้อมูล",
"toolResult": "ผลลัพธ์",
"statusApplied": "สมัครแล้ว",
"statusRejected": "ถูกปฏิเสธ",
"proposalApplied": "มีการเปลี่ยนแปลงเกิดขึ้น",
"proposalApplyFailed": "ไม่สามารถทำการเปลี่ยนแปลงได้",
"proposalRejectFailed": "ไม่สามารถปฏิเสธการเปลี่ยนแปลงได้",
"enableTitle": "เปิดใช้งานผู้ช่วย AI",
"enableDescription": "ให้ผู้ช่วยอ่านการตั้งค่าของคุณและแนะนำการเปลี่ยนแปลงเพื่อให้คุณอนุมัติ",
"readOnlyCommands": "อนุญาตคำสั่งวินิจฉัยแบบอ่านอย่างเดียว",
"readOnlyCommandsDescription": "ผู้ช่วยอาจเรียกใช้คำสั่งที่ปลอดภัยเพียงไม่กี่คำสั่ง เช่น df และ uptime โดยไม่ต้องถามทุกครั้ง คำสั่งใดๆ ที่เปลี่ยนแปลงสถานะของเซิร์ฟเวอร์จะถูกเสนอให้ดำเนินการก่อนเสมอ",
"modelDetectFailed": "ไม่สามารถติดต่อผู้ให้บริการได้ ดังนั้นนี่จึงเป็นรายการทั่วไป คุณสามารถพิมพ์ชื่อรุ่นใดก็ได้",
"modelCustom": "ป้อนชื่อรุ่น",
"modelRefresh": "รีเฟรชรายการ",
"modelPlaceholder": "เลือกหรือพิมพ์แบบจำลอง",
"attachHint": "พิมพ์ @ เพื่ออ้างอิงถึงโฮสต์ สนิปเป็ต หรือระบบอัตโนมัติ การเปลี่ยนแปลงที่เสนอจะถูกนำมาให้คุณอนุมัติ",
"runCommandTitle": "เรียกใช้คำสั่ง",
"assistant": "ผู้ช่วย AI",
"terminalPanelEmpty": "สอบถามผู้ช่วยเกี่ยวกับโฮสต์นี้ การเปลี่ยนแปลงสถานะใดๆ ต้องได้รับการอนุมัติจากคุณก่อน",
"terminalPanelPlaceholder": "สอบถามเกี่ยวกับโฮสต์รายนี้...",
"noProvidersConfigured": "ยังไม่ได้ตั้งค่าผู้ให้บริการ AI เพิ่มผู้ให้บริการในแท็บผู้ช่วย AI",
"runsInOpenTerminal": "ทำงานในเซสชันเทอร์มินัลที่เปิดอยู่ของคุณ",
"runInTerminal": "วิ่ง"
},
"nav": {
"ai": "AI"
},
"onboarding": {
"aiTitle": "ผู้ช่วย AI",
"aiIntro": "Termix อาจมีผู้ช่วย AI เสริม คุณสามารถเปลี่ยนการตั้งค่าได้ในภายหลัง",
"aiSkipTitle": "ปิดมันไว้",
"aiSkipDesc": "Termix ทำงานเหมือนเดิมทุกประการ ตัวช่วยยังคงซ่อนอยู่",
"aiEnableTitle": "เปิดใช้งาน",
"aiEnableDesc": "เพิ่มผู้ช่วยที่จะอ่านการตั้งค่าของคุณและแนะนำการเปลี่ยนแปลงที่คุณต้องอนุมัติ จำเป็นต้องมีผู้ให้บริการที่คุณจัดหาให้ เช่น Ollama ในพื้นที่ของคุณ",
"aiNote": "จะไม่มีการส่งข้อมูลใดๆ จนกว่าคุณจะเพิ่มผู้ให้บริการและส่งข้อความ"
},
"admin": {
"aiGloballyEnabled": "ผู้ช่วย AI",
"aiGloballyEnabledDesc": "อนุญาตให้ผู้ใช้เปิดใช้งานผู้ช่วย AI ได้ เมื่อปิดใช้งานอยู่ ผู้ช่วยจะถูกซ่อนและบล็อกไม่ให้ใครเห็น",
"aiPrivateEndpoints": "อนุญาตให้โฮสต์ AI ส่วนตัว",
"aiPrivateEndpointsDesc": "โฮสต์บนเครือข่ายส่วนตัวของคุณที่ผู้ใช้สามารถชี้ไปยังผู้ให้บริการได้ เช่น Ollama ที่ติดตั้งเอง คั่นด้วยเครื่องหมายจุลภาค",
"updateAiEnabledFailed": "ไม่สามารถอัปเดตการตั้งค่า AI ได้",
"updateAiEndpointsFailed": "ไม่สามารถอัปเดตโฮสต์ที่อนุญาตได้"
}
}
+78
View File
@@ -0,0 +1,78 @@
{
"ai": {
"title": "Asistan",
"send": "Göndermek",
"stop": "Durmak",
"save": "Kaydetmek",
"cancel": "İptal etmek",
"running": "koşma",
"approve": "Onaylamak",
"reject": "Reddetmek",
"inputPlaceholder": "Sunucularınız hakkında bilgi isteyin veya önereceğiniz bir değişikliği açıklayın.",
"selectProvider": "Bir sağlayıcı seçin",
"providerSettings": "Sağlayıcı ayarları",
"addProvider": "Sağlayıcı ekle",
"editProvider": "Sağlayıcıyı düzenle",
"removeProvider": "Sağlayıcıyı kaldır",
"providerType": "Sağlayıcı",
"providerLabel": "İsim",
"providerLabelPlaceholder": "Örneğin: Yerel Ollama",
"baseUrl": "Sunucu adresi",
"apiKey": "API anahtarı",
"defaultModel": "Varsayılan model",
"defaultModelPlaceholder": "Örneğin: llama3.1 veya claude-opus-5",
"labelRequired": "Bu sağlayıcıya bir isim verin.",
"providerSaveFailed": "Sağlayıcı kaydedilemedi.",
"providerUpdated": "Sağlayıcı güncellendi.",
"providerDeleteFailed": "Sağlayıcı kaldırılamadı.",
"privateEndpointHint": "Özel ağınızdaki adreslerin kullanılabilmesi için öncelikle yöneticinin sunucuya izin vermesi gerekmektedir.",
"providerOllama": "Ollama",
"providerAnthropic": "Antropik",
"providerOpenai": "OpenAI",
"providerGemini": "Google Gemini",
"providerOpenaiCompatible": "OpenAI uyumlu",
"toolArguments": "Giriş",
"toolResult": "Sonuç",
"statusApplied": "Uygulandı",
"statusRejected": "Reddedilmiş",
"proposalApplied": "Değişiklik uygulandı.",
"proposalApplyFailed": "Değişiklik uygulanamadı.",
"proposalRejectFailed": "Değişikliği reddedemedim.",
"enableTitle": "Yapay zekâ asistanını etkinleştirin",
"enableDescription": "Asistanın ayarlarınızı okumasını ve onaylamanız için değişiklik önerileri sunmasını sağlayın.",
"readOnlyCommands": "Yalnızca okuma amaçlı tanı komutlarına izin ver",
"readOnlyCommandsDescription": "Yardımcı program, her seferinde sormadan df ve uptime gibi kısa bir güvenli komut listesini çalıştırabilir. Sunucuda değişiklik yapan her şey her zaman önce önerilir.",
"modelDetectFailed": "Sağlayıcıya ulaşılamadı, bu nedenle bu genel bir listedir. İstediğiniz model adını yazabilirsiniz.",
"modelCustom": "Bir model adı girin",
"modelRefresh": "Listeyi yenile",
"modelPlaceholder": "Bir model seçin veya yazın.",
"attachHint": "Bir sunucuya, kod parçasına veya otomasyona referans vermek için @ işaretini kullanın. Onaylamanız için değişiklikler öneriliyor.",
"runCommandTitle": "Çalıştır komutu",
"assistant": "Yapay Zeka Asistanı",
"terminalPanelEmpty": "Bu ev sahibi hakkında asistana danışın. Durum değişikliği gerektiren her şey önce sizin onayınızı gerektirir.",
"terminalPanelPlaceholder": "Bu ev sahibi hakkında bilgi isteyin...",
"noProvidersConfigured": "Henüz hiçbir yapay zeka sağlayıcısı yapılandırılmadı. Yapay zeka asistanı sekmesinden bir tane ekleyin.",
"runsInOpenTerminal": "Açık terminal oturumunuzda çalışır.",
"runInTerminal": "Koşmak"
},
"nav": {
"ai": "Yapay zeka"
},
"onboarding": {
"aiTitle": "Yapay zeka asistanı",
"aiIntro": "Termix isteğe bağlı bir yapay zeka asistanı içerebilir. Bunu daha sonra ayarlarınızdan değiştirebilirsiniz.",
"aiSkipTitle": "Kapalı bırakın",
"aiSkipDesc": "Termix şu anki haliyle çalışıyor. Asistan gizli kalıyor.",
"aiEnableTitle": "Açın",
"aiEnableDesc": "Ayarlarınızı okuyan ve onaylamanız halinde değişiklikler öneren bir asistan ekler. Yerel bir Ollama gibi, sizin sağladığınız bir sağlayıcıya ihtiyaç duyar.",
"aiNote": "Bir sağlayıcı ekleyip mesaj gönderene kadar hiçbir şey hiçbir yere gönderilmez."
},
"admin": {
"aiGloballyEnabled": "Yapay zeka asistanı",
"aiGloballyEnabledDesc": "Kullanıcıların yapay zeka asistanını açmasına izin verin. Bu özellik kapalıyken, asistan gizlenir ve herkes için engellenir.",
"aiPrivateEndpoints": "Özel yapay zeka sunucularına izin verildi.",
"aiPrivateEndpointsDesc": "Kullanıcıların bir sağlayıcıya yönlendirebileceği, özel ağınızdaki sunucular (örneğin, kendi sunucunuzda barındırdığınız Ollama). Bunları virgülle ayırın.",
"updateAiEnabledFailed": "Yapay zeka ayarları güncellenemedi.",
"updateAiEndpointsFailed": "İzin verilen sunucular güncellenemedi."
}
}
+78
View File
@@ -0,0 +1,78 @@
{
"ai": {
"title": "Помічник",
"send": "Надіслати",
"stop": "СТІЙ",
"save": "Зберегти",
"cancel": "Скасувати",
"running": "біг",
"approve": "Схвалити",
"reject": "Відхилити",
"inputPlaceholder": "Запитайте про ваші сервери або опишіть зміну, яку ви хочете запропонувати",
"selectProvider": "Виберіть постачальника",
"providerSettings": "Налаштування постачальника",
"addProvider": "Додати постачальника",
"editProvider": "Редагувати постачальника",
"removeProvider": "Видалити постачальника",
"providerType": "Постачальник",
"providerLabel": "Ім'я",
"providerLabelPlaceholder": "Наприклад: Місцевий Оллама",
"baseUrl": "Адреса сервера",
"apiKey": "Ключ API",
"defaultModel": "Модель за замовчуванням",
"defaultModelPlaceholder": "Наприклад: llama3.1 або claude-opus-5",
"labelRequired": "Введіть назву цього постачальника",
"providerSaveFailed": "Не вдалося зберегти постачальника",
"providerUpdated": "Постачальника оновлено",
"providerDeleteFailed": "Не вдалося видалити постачальника",
"privateEndpointHint": "Адреси у вашій приватній мережі спочатку потребують дозволу адміністратора хоста.",
"providerOllama": "Оллама",
"providerAnthropic": "Антропний",
"providerOpenai": "OpenAI",
"providerGemini": "Google Близнюки",
"providerOpenaiCompatible": "Сумісний з OpenAI",
"toolArguments": "Вхід",
"toolResult": "Результат",
"statusApplied": "Застосовано",
"statusRejected": "Відхилено",
"proposalApplied": "Зміну застосовано",
"proposalApplyFailed": "Не вдалося застосувати зміну",
"proposalRejectFailed": "Не вдалося відхилити зміну",
"enableTitle": "Увімкнути помічника зі штучним інтелектом",
"enableDescription": "Дозвольте асистенту прочитати ваші налаштування та запропонувати зміни для вашого схвалення.",
"readOnlyCommands": "Дозволити діагностичні команди лише для читання",
"readOnlyCommandsDescription": "Помічник може виконувати короткий список безпечних команд, таких як df та uptime, не запитуючи щоразу. Будь-які зміни на сервері завжди пропонуються першими.",
"modelDetectFailed": "Не вдалося зв’язатися з постачальником, тому це загальний список. Ви можете ввести будь-яку назву моделі.",
"modelCustom": "Введіть назву моделі",
"modelRefresh": "Оновити список",
"modelPlaceholder": "Виберіть або введіть модель",
"attachHint": "Введіть @, щоб посилатися на хост, фрагмент або автоматизацію. Зміни пропонуються для вашого схвалення.",
"runCommandTitle": "Виконати команду",
"assistant": "Помічник зі штучним інтелектом",
"terminalPanelEmpty": "Запитайте асистента про цього хоста. Будь-яка зміна стану потребує вашого попереднього схвалення.",
"terminalPanelPlaceholder": "Запитайте про цього хоста...",
"noProvidersConfigured": "Постачальника штучного інтелекту ще не налаштовано. Додайте його на вкладці помічника штучного інтелекту.",
"runsInOpenTerminal": "Працює у вашому відкритому термінальному сеансі.",
"runInTerminal": "Бігти"
},
"nav": {
"ai": "Штучний інтелект"
},
"onboarding": {
"aiTitle": "Асистент зі штучним інтелектом",
"aiIntro": "Termix може включати додатковий помічник зі штучним інтелектом. Ви можете змінити це пізніше в налаштуваннях.",
"aiSkipTitle": "Залиште це вимкненим",
"aiSkipDesc": "Termix працює як і зараз. Помічник залишається прихованим.",
"aiEnableTitle": "Увімкніть це",
"aiEnableDesc": "Додає помічника, який читає ваші налаштування та пропонує зміни, які ви схвалюєте. Потрібен постачальник, якого ви надаєте, наприклад, місцевий Ollama.",
"aiNote": "Нічого нікуди не надсилається, доки ви не додасте постачальника та не надішлете повідомлення."
},
"admin": {
"aiGloballyEnabled": "Асистент зі штучним інтелектом",
"aiGloballyEnabledDesc": "Дозвольте користувачам увімкнути помічника зі штучним інтелектом. Коли це вимкнено, помічник прихований і заблокований для всіх.",
"aiPrivateEndpoints": "Дозволені приватні хости ШІ",
"aiPrivateEndpointsDesc": "Хости у вашій приватній мережі, на які користувачі можуть вказувати провайдера, наприклад, Ollama з власним хостингом. Розділяйте їх комами.",
"updateAiEnabledFailed": "Не вдалося оновити налаштування штучного інтелекту",
"updateAiEndpointsFailed": "Не вдалося оновити дозволені хости"
}
}
+78
View File
@@ -0,0 +1,78 @@
{
"ai": {
"title": "Trợ lý",
"send": "Gửi",
"stop": "Dừng lại",
"save": "Cứu",
"cancel": "Hủy bỏ",
"running": "đang chạy",
"approve": "Chấp thuận",
"reject": "Từ chối",
"inputPlaceholder": "Hãy hỏi về máy chủ của bạn, hoặc mô tả một thay đổi mà bạn muốn đề xuất.",
"selectProvider": "Chọn nhà cung cấp",
"providerSettings": "Cài đặt nhà cung cấp",
"addProvider": "Thêm nhà cung cấp",
"editProvider": "Chỉnh sửa nhà cung cấp",
"removeProvider": "Xóa nhà cung cấp",
"providerType": "Nhà cung cấp",
"providerLabel": "Tên",
"providerLabelPlaceholder": "Ví dụ: Ollama địa phương",
"baseUrl": "Địa chỉ máy chủ",
"apiKey": "Khóa API",
"defaultModel": "Mô hình mặc định",
"defaultModelPlaceholder": "Ví dụ: llama3.1 hoặc claude-opus-5",
"labelRequired": "Đặt tên cho nhà cung cấp này",
"providerSaveFailed": "Không thể lưu nhà cung cấp",
"providerUpdated": "Nhà cung cấp đã cập nhật",
"providerDeleteFailed": "Không thể xóa nhà cung cấp",
"privateEndpointHint": "Các địa chỉ trên mạng riêng của bạn cần được quản trị viên cho phép truy cập trước.",
"providerOllama": "Ollama",
"providerAnthropic": "Nhân loại",
"providerOpenai": "OpenAI",
"providerGemini": "Google Gemini",
"providerOpenaiCompatible": "Tương thích với OpenAI",
"toolArguments": "Đầu vào",
"toolResult": "Kết quả",
"statusApplied": "Đã áp dụng",
"statusRejected": "Vật bị loại bỏ",
"proposalApplied": "Thay đổi đã được áp dụng",
"proposalApplyFailed": "Không thể áp dụng thay đổi",
"proposalRejectFailed": "Không thể từ chối thay đổi",
"enableTitle": "Kích hoạt trợ lý AI",
"enableDescription": "Hãy để trợ lý đọc cấu hình của bạn và đề xuất các thay đổi để bạn phê duyệt.",
"readOnlyCommands": "Cho phép các lệnh chẩn đoán chỉ đọc",
"readOnlyCommandsDescription": "Trợ lý có thể chạy một danh sách ngắn các lệnh an toàn như df và uptime mà không cần hỏi mỗi lần. Bất kỳ lệnh nào thay đổi máy chủ đều được đề xuất trước tiên.",
"modelDetectFailed": "Không thể liên lạc được với nhà cung cấp, vì vậy đây là danh sách chung. Bạn có thể nhập bất kỳ tên mẫu nào.",
"modelCustom": "Nhập tên mẫu",
"modelRefresh": "Làm mới danh sách",
"modelPlaceholder": "Chọn hoặc nhập mô hình",
"attachHint": "Nhập @ để tham chiếu đến máy chủ, đoạn mã hoặc quy trình tự động hóa. Các thay đổi được đề xuất để bạn phê duyệt.",
"runCommandTitle": "Chạy lệnh",
"assistant": "Trợ lý AI",
"terminalPanelEmpty": "Hãy hỏi trợ lý về máy chủ này. Bất cứ thay đổi nào về trạng thái đều cần sự chấp thuận của bạn trước.",
"terminalPanelPlaceholder": "Hãy hỏi về chủ nhà này...",
"noProvidersConfigured": "Chưa có nhà cung cấp AI nào được cấu hình. Hãy thêm một nhà cung cấp trong tab trợ lý AI.",
"runsInOpenTerminal": "Chương trình này chạy trong cửa sổ dòng lệnh đang mở của bạn.",
"runInTerminal": "Chạy"
},
"nav": {
"ai": "Trí tuệ nhân tạo"
},
"onboarding": {
"aiTitle": "Trợ lý AI",
"aiIntro": "Termix có thể bao gồm trợ lý AI tùy chọn. Bạn có thể thay đổi điều này sau trong phần cài đặt.",
"aiSkipTitle": "Đừng làm vậy",
"aiSkipDesc": "Termix vẫn hoạt động như hiện tại. Trợ lý ảo vẫn ẩn mình.",
"aiEnableTitle": "Bật nó lên",
"aiEnableDesc": "Thêm một trợ lý đọc cấu hình của bạn và đề xuất các thay đổi mà bạn chấp thuận. Cần có nhà cung cấp do bạn chỉ định, chẳng hạn như Ollama địa phương.",
"aiNote": "Không có dữ liệu nào được gửi đi cho đến khi bạn thêm nhà cung cấp và gửi tin nhắn."
},
"admin": {
"aiGloballyEnabled": "Trợ lý AI",
"aiGloballyEnabledDesc": "Cho phép người dùng bật trợ lý AI. Khi trợ lý này tắt, nó sẽ bị ẩn và chặn đối với tất cả mọi người.",
"aiPrivateEndpoints": "Cho phép máy chủ AI riêng",
"aiPrivateEndpointsDesc": "Các máy chủ trên mạng riêng của bạn mà người dùng có thể trỏ đến nhà cung cấp, chẳng hạn như Ollama tự lưu trữ. Phân tách chúng bằng dấu phẩy.",
"updateAiEnabledFailed": "Không thể cập nhật cài đặt AI.",
"updateAiEndpointsFailed": "Không thể cập nhật các máy chủ được cho phép"
}
}
+78
View File
@@ -0,0 +1,78 @@
{
"ai": {
"title": "助手",
"send": "发送",
"stop": "停止",
"save": "保存",
"cancel": "取消",
"running": "运行中",
"approve": "批准",
"reject": "拒绝",
"inputPlaceholder": "询问服务器相关情况,或描述要提出的变更建议",
"selectProvider": "选择服务提供商",
"providerSettings": "提供商设置",
"addProvider": "添加提供商",
"editProvider": "编辑提供商",
"removeProvider": "移除提供者",
"providerType": "提供商",
"providerLabel": "名称",
"providerLabelPlaceholder": "例如:本地 Ollama",
"baseUrl": "服务器地址",
"apiKey": "API 密钥",
"defaultModel": "默认模型",
"defaultModelPlaceholder": "例如:llama3.1 或 claude-opus-5",
"labelRequired": "给此提供商起个名字",
"providerSaveFailed": "无法保存提供商",
"providerUpdated": "提供商已更新",
"providerDeleteFailed": "无法移除提供商",
"privateEndpointHint": "您私有网络上的地址需要管理员先允许主机访问。",
"providerOllama": "Ollama",
"providerAnthropic": "Anthropic",
"providerOpenai": "OpenAI",
"providerGemini": "Google Gemini",
"providerOpenaiCompatible": "兼容 OpenAI",
"toolArguments": "输入",
"toolResult": "结果",
"statusApplied": "已应用",
"statusRejected": "已拒绝",
"proposalApplied": "变更已生效",
"proposalApplyFailed": "无法应用更改",
"proposalRejectFailed": "无法拒绝更改",
"enableTitle": "启用 AI 助手",
"enableDescription": "让助手读取您的设置并提出更改建议,供您批准。",
"readOnlyCommands": "允许只读诊断命令",
"readOnlyCommandsDescription": "助手可能会在不每次都询问的情况下运行一些安全的命令,例如 `df` 和 `uptime`。任何会更改服务器的操作都会优先被提出。",
"modelDetectFailed": "无法联系到提供商,所以这里提供的是通用列表。您可以输入任何模型名称。",
"modelCustom": "请输入模型名称",
"modelRefresh": "刷新列表",
"modelPlaceholder": "选择或输入模型",
"attachHint": "输入 @ 符号可引用主机、代码片段或自动化流程。系统会提出更改建议,供您批准。",
"runCommandTitle": "运行命令",
"assistant": "人工智能助手",
"terminalPanelEmpty": "向助理询问有关此房东的信息。任何状态变更都需要您先批准。",
"terminalPanelPlaceholder": "询问一下这位房东的情况……",
"noProvidersConfigured": "尚未配置任何 AI 提供商。请在 AI 助手选项卡中添加。",
"runsInOpenTerminal": "在打开的终端会话中运行。",
"runInTerminal": "跑步"
},
"nav": {
"ai": "AI"
},
"onboarding": {
"aiTitle": "AI 助手",
"aiIntro": "Termix 可以包含一个可选的 AI 助手。您可以稍后在设置中更改此选项。",
"aiSkipTitle": "禁用",
"aiSkipDesc": "Termix 正常工作,AI 助手将保持隐藏状态。",
"aiEnableTitle": "启用",
"aiEnableDesc": "启用 AI 助手,它将分析您的配置并提供优化建议。需自行配置服务商,例如本地运行的 Ollama。",
"aiNote": "在您配置服务商并主动发送消息之前,绝不会向外发送任何数据。"
},
"admin": {
"aiGloballyEnabled": "AI 助手",
"aiGloballyEnabledDesc": "允许用户开启 AI 助手。关闭此功能后,助手将被隐藏,所有用户均无法使用。",
"aiPrivateEndpoints": "允许的私有 AI 主机",
"aiPrivateEndpointsDesc": "用户可能将服务提供商指向的私有网络上的主机,例如自托管的 Ollama。用逗号分隔。",
"updateAiEnabledFailed": "无法更新 AI 设置",
"updateAiEndpointsFailed": "无法更新允许的主机列表。"
}
}
+78
View File
@@ -0,0 +1,78 @@
{
"ai": {
"title": "助理",
"send": "傳送",
"stop": "停止",
"save": "節省",
"cancel": "取消",
"running": "跑步",
"approve": "核准",
"reject": "拒絕",
"inputPlaceholder": "詢問伺服器相關情況,或描述要提出的變更建議",
"selectProvider": "選擇服務提供者",
"providerSettings": "提供者設定",
"addProvider": "新增提供者",
"editProvider": "編輯提供者",
"removeProvider": "移除提供者",
"providerType": "提供者",
"providerLabel": "姓名",
"providerLabelPlaceholder": "例如:本地奧拉馬",
"baseUrl": "伺服器位址",
"apiKey": "API金鑰",
"defaultModel": "預設模型",
"defaultModelPlaceholder": "例如:llama3.1 或 claude-opus-5",
"labelRequired": "給這家服務提供者取個名字",
"providerSaveFailed": "無法儲存提供者",
"providerUpdated": "提供者已更新",
"providerDeleteFailed": "無法移除提供者",
"privateEndpointHint": "您私有網路上的位址需要管理員先允許主機存取。",
"providerOllama": "奧拉瑪",
"providerAnthropic": "人類學",
"providerOpenai": "OpenAI",
"providerGemini": "Google Gemini",
"providerOpenaiCompatible": "相容 OpenAI",
"toolArguments": "輸入",
"toolResult": "結果",
"statusApplied": "應用",
"statusRejected": "已拒絕",
"proposalApplied": "變更已生效",
"proposalApplyFailed": "無法套用變更",
"proposalRejectFailed": "無法拒絕更改",
"enableTitle": "啟用人工智慧助手",
"enableDescription": "請助手讀取您的設定並提出更改建議,供您批准。",
"readOnlyCommands": "允許只讀診斷命令",
"readOnlyCommandsDescription": "助手可能會在不每次都詢問的情況下執行一些安全的命令,例如 `df` 和 `uptime`。任何會更改伺服器的操作都會優先被提出。",
"modelDetectFailed": "無法聯繫到供應商,所以這裡提供的是通用清單。您可以輸入任何型號名稱。",
"modelCustom": "請輸入型號名稱",
"modelRefresh": "重新整理清單",
"modelPlaceholder": "選擇或輸入型號",
"attachHint": "輸入 @ 符號可引用主機、程式碼片段或自動化流程。系統會提出更改建議,供您批准。",
"runCommandTitle": "運行命令",
"assistant": "人工智慧助手",
"terminalPanelEmpty": "向助理詢問有關此房東的資訊。任何狀態變更都需要您先批准。",
"terminalPanelPlaceholder": "詢問一下這位房東的狀況…",
"noProvidersConfigured": "尚未配置任何 AI 提供者。請在 AI 助理標籤中新增。",
"runsInOpenTerminal": "在開啟的終端機會話中運行。",
"runInTerminal": "跑步"
},
"nav": {
"ai": "人工智慧"
},
"onboarding": {
"aiTitle": "人工智慧助手",
"aiIntro": "Termix 可以包含一個可選的 AI 助手。您可以稍後在設定中變更此選項。",
"aiSkipTitle": "丟開",
"aiSkipDesc": "Termix 的運作方式與現在相同。助手保持隱藏狀態。",
"aiEnableTitle": "打開它",
"aiEnableDesc": "添加一個助手,它可以讀取您的設定並提出您批准的更改建議。需要您提供服務提供者,例如當地的 Ollama 服務提供者。",
"aiNote": "在您新增服務提供者並發送訊息之前,任何內容都不會發送到任何地方。"
},
"admin": {
"aiGloballyEnabled": "人工智慧助手",
"aiGloballyEnabledDesc": "允許用戶開啟AI助理。關閉此功能後,助手將被隱藏,所有使用者均無法使用。",
"aiPrivateEndpoints": "允許的私有人工智慧主機",
"aiPrivateEndpointsDesc": "使用者可能將服務提供者指向的私有網路上的主機,例如自架的 Ollama。用逗號分隔。",
"updateAiEnabledFailed": "無法更新AI設定",
"updateAiEndpointsFailed": "無法更新允許的主機清單。"
}
}
+17 -1
View File
@@ -87,7 +87,23 @@
"descriptionKey": "permissions.secrets.share.description",
"defaultRoles": ["user"]
}
]
],
"settings": {
"admin": [
{
"key": "access",
"type": "custom",
"component": "access"
}
],
"user": [
{
"key": "assistant",
"type": "custom",
"component": "assistant"
}
]
}
},
"locales": "locales",
"platforms": ["linux", "win32", "darwin"]
@@ -0,0 +1,82 @@
import { useEffect, useState } from "react";
import { Check } from "lucide-react";
import { useTranslation } from "@termix/plugin-sdk/frontend";
import { setAssistantEnabledForUser } from "./settings/AiUserSettings";
/**
* Asks once, plainly.
*
* "Leave it off" is preselected and applied on mount, so someone who clicks
* straight through ends up without the assistant rather than with it. Both
* options are described in the same flat register and neither is marked
* recommended: this is a choice, not a pitch.
*/
export function AiAssistantStep() {
const { t } = useTranslation();
const [enabled, setEnabled] = useState(false);
function apply(next: boolean) {
setEnabled(next);
setAssistantEnabledForUser(next);
}
// Skipping the step entirely still has to mean off, not unset.
useEffect(() => {
apply(false);
}, []);
const options = [
{
value: false,
title: t("onboarding.aiSkipTitle"),
description: t("onboarding.aiSkipDesc"),
},
{
value: true,
title: t("onboarding.aiEnableTitle"),
description: t("onboarding.aiEnableDesc"),
},
];
return (
<div className="flex flex-col gap-3">
<p className="text-xs text-muted-foreground">{t("onboarding.aiIntro")}</p>
<div className="flex flex-col gap-2">
{options.map((option) => (
<button
key={String(option.value)}
type="button"
onClick={() => apply(option.value)}
className={`flex items-start gap-2.5 border p-2.5 text-left ${
enabled === option.value
? "border-accent-brand bg-accent-brand/10"
: "border-border bg-card"
}`}
>
<div className="flex min-w-0 flex-col gap-0.5">
<span className="text-xs font-medium">{option.title}</span>
<span className="text-[10px] leading-snug text-muted-foreground">
{option.description}
</span>
</div>
{/*
Always rendered and only made visible when selected, so picking an
option never reflows the text beside it.
*/}
<Check
size={14}
className={`ml-auto mt-0.5 shrink-0 ${
enabled === option.value ? "opacity-100" : "opacity-0"
}`}
/>
</button>
))}
</div>
<p className="text-[10px] text-muted-foreground/70">
{t("onboarding.aiNote")}
</p>
</div>
);
}
+23
View File
@@ -0,0 +1,23 @@
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
interface AiMessageProps {
role: "user" | "assistant";
content: string;
}
export function AiMessage({ role, content }: AiMessageProps) {
if (role === "user") {
return (
<div className="rounded-none border border-border bg-muted px-3 py-2 text-sm whitespace-pre-wrap break-words">
{content}
</div>
);
}
return (
<div className="text-sm leading-relaxed [&_a]:underline [&_code]:font-mono [&_code]:text-xs [&_li]:my-0.5 [&_ol]:my-2 [&_ol]:list-decimal [&_ol]:pl-5 [&_p]:my-2 [&_pre]:overflow-x-auto [&_pre]:rounded-none [&_pre]:border [&_pre]:border-border [&_pre]:bg-muted [&_pre]:p-2 [&_ul]:my-2 [&_ul]:list-disc [&_ul]:pl-5">
<ReactMarkdown remarkPlugins={[remarkGfm]}>{content}</ReactMarkdown>
</div>
);
}
+533
View File
@@ -0,0 +1,533 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "@termix/plugin-sdk/frontend";
import { Loader2, Send, Settings2, Sparkles, Square } from "lucide-react";
import { Button } from "@/components/button";
import { Textarea } from "@/components/textarea";
import { saveUserPreferences } from "@/main-axios";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/select";
import {
getAiConversation,
getAiProviders,
getAiStatus,
type AiProposal,
type AiProvider,
} from "./ai-api";
import { AiMessage } from "./AiMessage";
import { AiProviderSettings } from "./AiProviderSettings";
import { AiToolCall } from "./AiToolCall";
import { ProposalCard } from "./ProposalCard";
import { useAiStream, type ToolActivity } from "./use-ai-stream";
import {
activeMentionQuery,
useMentions,
type MentionItem,
} from "./useMentions";
import { mentionLabel } from "./labels";
interface HistoryEntry {
role: "user" | "assistant";
content: string;
}
/** One entry in the rendered conversation, in the order it happened. */
type TimelineItem =
| {
kind: "message";
key: string;
role: "user" | "assistant";
content: string;
}
| { kind: "tool"; key: string; tool: ToolActivity }
| { kind: "proposal"; key: string; proposal: AiProposal };
export function AiPanel({ activeTab }: { activeTab?: string | null }) {
const { t } = useTranslation();
const { state, send, stop } = useAiStream();
const [providers, setProviders] = useState<AiProvider[]>([]);
const [providerId, setProviderId] = useState<number | null>(null);
const [loading, setLoading] = useState(true);
const [showSettings, setShowSettings] = useState(false);
const [input, setInput] = useState("");
const [history, setHistory] = useState<HistoryEntry[]>([]);
const [resolvedProposals, setResolvedProposals] = useState<
Record<number, { status: "applied" | "rejected"; resultSummary?: string }>
>({});
const scrollRef = useRef<HTMLDivElement>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const [mention, setMention] = useState<{
query: string;
start: number;
} | null>(null);
const [mentionIndex, setMentionIndex] = useState(0);
const { search: searchMentions } = useMentions(Boolean(providerId));
const mentionMatches = mention ? searchMentions(mention.query) : [];
const updateMentionQuery = (value: string, caret: number) => {
const next = activeMentionQuery(value, caret);
setMention(next);
setMentionIndex(0);
};
/**
* Replaces the "@partial" under the caret with the chosen name. The mention
* is plain text: the assistant still has to call a read tool to look the
* item up, so nothing is attached to the prompt behind the user's back.
*/
const insertMention = (item: MentionItem) => {
if (!mention) return;
const caret = textareaRef.current?.selectionStart ?? input.length;
const before = input.slice(0, mention.start);
const after = input.slice(caret);
const inserted = `@${item.label} `;
setInput(`${before}${inserted}${after}`);
setMention(null);
requestAnimationFrame(() => {
const position = before.length + inserted.length;
textareaRef.current?.focus();
textareaRef.current?.setSelectionRange(position, position);
});
};
const loadProviders = useCallback(async (selectId?: number) => {
try {
const list = await getAiProviders();
setProviders(list);
setProviderId((current) => {
// A provider that was just added becomes the active one, so there is
// no second step between creating it and being able to use it.
if (selectId && list.some((entry) => entry.id === selectId)) {
return selectId;
}
// Fall back to the first if the current selection has gone away.
if (current && list.some((entry) => entry.id === current)) {
return current;
}
return list[0]?.id ?? null;
});
return list;
} catch {
setProviders([]);
return [];
}
}, []);
useEffect(() => {
let cancelled = false;
(async () => {
try {
const status = await getAiStatus();
if (cancelled) return;
// Opening the panel is consent to use it. Without this every write
// 403s through the gate, which surfaces as a bare permission error
// rather than anything the user can act on.
if (status.globallyEnabled && !status.enabled) {
await saveUserPreferences({ aiAssistantEnabled: true });
}
if (!status.globallyEnabled) return;
const list = await loadProviders();
// With nothing configured there is nothing to chat with, so go
// straight to the form instead of showing a card that just vanishes.
if (!cancelled && list.length === 0) setShowSettings(true);
} finally {
if (!cancelled) setLoading(false);
}
})();
return () => {
cancelled = true;
};
}, [loadProviders]);
useEffect(() => {
scrollRef.current?.scrollTo({
top: scrollRef.current.scrollHeight,
behavior: "smooth",
});
}, [state.assistantText, state.tools.length, history.length]);
const reloadHistory = useCallback(
async (conversationId: number | null, reply: string) => {
// Reload from the server so the transcript is the stored one rather
// than a client-side reconstruction.
if (conversationId) {
try {
const loaded = await getAiConversation(conversationId);
setHistory(
loaded.messages
.filter((entry) => entry.role !== "tool")
.map((entry) => ({
role: entry.role as "user" | "assistant",
content: entry.content,
})),
);
return;
} catch {
// Fall through to keeping the streamed copy.
}
}
// No stored copy to fall back on, so the streamed reply is appended
// locally. Without this it would vanish when the next turn resets the
// stream state.
if (reply) {
setHistory((prev) => [...prev, { role: "assistant", content: reply }]);
}
},
[],
);
async function handleSend() {
const message = input.trim();
if (!message || !providerId || state.streaming) return;
setInput("");
setHistory((prev) => [...prev, { role: "user", content: message }]);
await send({
message,
providerId,
conversationId: state.conversationId,
activeTab,
onComplete: reloadHistory,
});
}
/**
* Records the outcome on the card and tells the assistant what happened.
*
* Without the follow-up the run just stops: the assistant proposed a
* command, the user approved it, and the model never learns it produced
* output, so it sits there as if the tool were still running.
*/
function handleProposalResolved(
id: number,
status: "applied" | "rejected",
resultSummary?: string,
) {
setResolvedProposals((prev) => ({
...prev,
[id]: { status, resultSummary },
}));
if (!providerId || state.streaming) return;
const proposal = state.proposals.find((entry) => entry.id === id);
const label = proposal?.summary ?? proposal?.kind ?? `proposal ${id}`;
const followUp =
status === "applied"
? resultSummary
? `I approved "${label}". Result:\n\n${resultSummary}`
: `I approved "${label}". It completed.`
: `I rejected "${label}". Do not retry it unless I ask.`;
setHistory((prev) => [...prev, { role: "user", content: followUp }]);
void send({
message: followUp,
providerId,
conversationId: state.conversationId,
activeTab,
onComplete: reloadHistory,
});
}
const proposals: AiProposal[] = state.proposals.map((proposal) => {
const resolved = resolvedProposals[proposal.id];
if (!resolved) return proposal;
return {
...proposal,
status: resolved.status,
resultSummary: resolved.resultSummary ?? proposal.resultSummary,
};
});
/**
* The reply exists in two places once a turn finishes: the streamed text in
* `state`, and the stored copy that `onComplete` reloads into `history`.
* Whichever arrives is shown exactly once, and the stored copy wins because
* it sits in the right place in the timeline.
*/
const last = history[history.length - 1];
const historyEndsWithThisReply =
state.assistantText.length > 0 &&
last?.role === "assistant" &&
last.content === state.assistantText;
const streamingReply = historyEndsWithThisReply ? "" : state.assistantText;
/**
* One ordered list rather than three stacked blocks.
*
* Tool calls happen before the reply that describes them, but once a turn
* ends its reply moves into `history`, which used to render above a separate
* tools block. The tool call appeared to jump below the answer it produced.
* Splicing the current turn's activity in ahead of its own reply keeps the
* order stable across that handover.
*/
const timeline: TimelineItem[] = [];
const trailingReply = historyEndsWithThisReply ? history.length - 1 : -1;
history.forEach((entry, index) => {
if (index === trailingReply) return;
timeline.push({
kind: "message",
key: `history-${index}`,
role: entry.role,
content: entry.content,
});
});
for (const tool of state.tools) {
timeline.push({ kind: "tool", key: tool.id, tool });
}
if (trailingReply >= 0) {
timeline.push({
kind: "message",
key: `history-${trailingReply}`,
role: "assistant",
content: history[trailingReply].content,
});
} else if (streamingReply) {
timeline.push({
kind: "message",
key: "streaming",
role: "assistant",
content: streamingReply,
});
}
for (const proposal of proposals) {
timeline.push({
kind: "proposal",
key: `proposal-${proposal.id}`,
proposal,
});
}
if (loading) {
return (
<div className="flex h-full items-center justify-center">
<Loader2 className="animate-spin text-muted-foreground" size={20} />
</div>
);
}
return (
<div className="flex h-full flex-col">
{/*
The panel also lives in the narrow sidebar, so the title and the
provider picker get their own rows rather than competing for one line.
*/}
<div className="border-b border-border">
<div className="flex items-center gap-2 px-3 py-2">
<Sparkles size={16} className="shrink-0" />
<span className="truncate text-sm font-medium">{t("ai.title")}</span>
<a
href="https://docs.termix.site/features/ai/overview"
target="_blank"
rel="noreferrer"
className="ml-auto shrink-0 text-[10px] text-accent-brand hover:underline"
>
{t("hosts.docsLink")}
</a>
<Button
size="sm"
variant="ghost"
className="shrink-0"
onClick={() => setShowSettings((value) => !value)}
aria-label={t("ai.providerSettings")}
>
<Settings2 size={14} />
</Button>
</div>
{providers.length > 0 && (
<div className="flex items-center gap-2 px-3 pb-2">
<Select
value={providerId ? String(providerId) : undefined}
onValueChange={(value) => setProviderId(Number(value))}
>
<SelectTrigger className="h-7 min-w-0 flex-1 rounded-none text-xs">
<SelectValue placeholder={t("ai.selectProvider")} />
</SelectTrigger>
<SelectContent>
{providers.map((provider) => (
<SelectItem key={provider.id} value={String(provider.id)}>
{provider.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
</div>
{showSettings && (
<div className="border-b border-border p-3">
<AiProviderSettings
providers={providers}
onChanged={loadProviders}
// Adding the first provider is the reason the form opened, so it
// closes once that is done and the chat becomes usable.
onAdded={() => setShowSettings(false)}
/>
</div>
)}
<div ref={scrollRef} className="flex-1 space-y-3 overflow-y-auto p-3">
{timeline.map((item) => {
if (item.kind === "message") {
return (
<AiMessage
key={item.key}
role={item.role}
content={item.content}
/>
);
}
if (item.kind === "tool") {
return (
<AiToolCall
key={item.key}
tool={item.tool}
streaming={state.streaming}
/>
);
}
return (
<ProposalCard
key={item.key}
proposal={item.proposal}
onResolved={handleProposalResolved}
/>
);
})}
{state.error && (
<div className="rounded-none border border-destructive/40 bg-destructive/10 px-3 py-2 text-sm text-destructive">
{state.error}
</div>
)}
</div>
<div className="relative border-t border-border p-3">
{mentionMatches.length > 0 && (
<div className="absolute bottom-full left-3 right-3 z-10 max-h-56 overflow-y-auto border border-border bg-popover shadow-md">
{mentionMatches.map((item, index) => (
<button
key={`${item.kind}-${item.id}`}
type="button"
className={`flex w-full items-center gap-2 px-2 py-1.5 text-left text-xs ${
index === mentionIndex ? "bg-muted" : ""
}`}
onMouseDown={(event) => {
// mousedown, so the textarea does not lose the caret first.
event.preventDefault();
insertMention(item);
}}
>
<span className="shrink-0 text-[10px] uppercase text-muted-foreground">
{mentionLabel(item.kind)}
</span>
<span className="truncate font-medium">{item.label}</span>
{item.detail && (
<span className="ml-auto shrink-0 truncate text-[10px] text-muted-foreground">
{item.detail}
</span>
)}
</button>
))}
</div>
)}
<Textarea
ref={textareaRef}
className="min-h-[64px] resize-none rounded-none text-sm"
value={input}
onChange={(event) => {
setInput(event.target.value);
updateMentionQuery(
event.target.value,
event.target.selectionStart ?? 0,
);
}}
onKeyDown={(event) => {
if (mentionMatches.length > 0) {
if (event.key === "ArrowDown") {
event.preventDefault();
setMentionIndex((i) => (i + 1) % mentionMatches.length);
return;
}
if (event.key === "ArrowUp") {
event.preventDefault();
setMentionIndex(
(i) =>
(i - 1 + mentionMatches.length) % mentionMatches.length,
);
return;
}
if (event.key === "Enter" || event.key === "Tab") {
event.preventDefault();
insertMention(mentionMatches[mentionIndex]);
return;
}
if (event.key === "Escape") {
event.preventDefault();
setMention(null);
return;
}
}
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
void handleSend();
}
}}
placeholder={t("ai.inputPlaceholder")}
disabled={!providerId}
/>
{/*
The hint gets its own line and wraps: sharing a row with the send
button left it truncated to an ellipsis in the sidebar, where the
panel is at its narrowest.
*/}
<p className="mt-2 text-[11px] leading-snug text-muted-foreground">
{t("ai.attachHint")}
</p>
<div className="mt-1.5 flex items-center justify-end gap-2">
{state.streaming ? (
<Button size="sm" variant="outline" onClick={stop}>
<Square size={14} />
{t("ai.stop")}
</Button>
) : (
<Button
size="sm"
variant="outline"
className="border-accent-brand/40 text-accent-brand hover:bg-accent-brand/10 hover:text-accent-brand"
disabled={!input.trim() || !providerId}
onClick={() => void handleSend()}
>
<Send size={14} />
{t("ai.send")}
</Button>
)}
</div>
</div>
</div>
);
}
@@ -0,0 +1,552 @@
import { getErrorMessage } from "@/lib/error-message";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "@termix/plugin-sdk/frontend";
import { toast } from "sonner";
import { Loader2, Pencil, Plus, RefreshCw, Trash2 } from "lucide-react";
import { Button } from "@/components/button";
import { Input } from "@/components/input";
import { Label } from "@/components/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/select";
import {
createAiProvider,
deleteAiProvider,
getAiProviderModels,
probeAiModels,
updateAiProvider,
type AiProvider,
type AiProviderType,
} from "./ai-api";
const PROVIDER_TYPES: Array<{
value: AiProviderType;
labelKey: string;
needsBaseUrl: boolean;
needsApiKey: boolean;
defaultBaseUrl?: string;
}> = [
{
value: "ollama",
labelKey: "ai.providerOllama",
needsBaseUrl: true,
needsApiKey: false,
defaultBaseUrl: "http://localhost:11434",
},
{
value: "anthropic",
labelKey: "ai.providerAnthropic",
needsBaseUrl: false,
needsApiKey: true,
},
{
value: "openai",
labelKey: "ai.providerOpenai",
needsBaseUrl: false,
needsApiKey: true,
},
{
value: "gemini",
labelKey: "ai.providerGemini",
needsBaseUrl: false,
needsApiKey: true,
},
{
value: "openai_compatible",
labelKey: "ai.providerOpenaiCompatible",
needsBaseUrl: true,
needsApiKey: false,
},
];
interface AiProviderSettingsProps {
providers: AiProvider[];
/** selectId names a provider that should become the active one. */
onChanged: (selectId?: number) => void;
onAdded?: () => void;
}
function AiProviderEditForm({
provider,
onSaved,
onCancel,
}: {
provider: AiProvider;
onSaved: () => void;
onCancel: () => void;
}) {
const { t } = useTranslation();
const [label, setLabel] = useState(provider.label);
const [defaultModel, setDefaultModel] = useState(provider.defaultModel ?? "");
const [models, setModels] = useState<string[]>([]);
const [customModel, setCustomModel] = useState(false);
const [detecting, setDetecting] = useState(false);
const [detectWarning, setDetectWarning] = useState<string | null>(null);
const [saving, setSaving] = useState(false);
const detectModels = useCallback(async () => {
setDetecting(true);
setDetectWarning(null);
try {
const detected = await getAiProviderModels(provider.id);
setModels(detected);
setCustomModel(!!defaultModel && !detected.includes(defaultModel));
} catch (error) {
setModels([]);
setCustomModel(true);
setDetectWarning(getErrorMessage(error, t("ai.modelDetectFailed")));
} finally {
setDetecting(false);
}
}, [provider.id, defaultModel, t]);
useEffect(() => {
void detectModels();
// The initial model value belongs to this provider. Subsequent edits must
// not trigger a provider model-list request on every keystroke.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [provider.id]);
async function handleSave() {
if (!label.trim()) {
toast.error(t("ai.labelRequired"));
return;
}
setSaving(true);
try {
await updateAiProvider(provider.id, {
label: label.trim(),
defaultModel: defaultModel.trim() || null,
});
toast.success(t("ai.providerUpdated"));
onSaved();
} catch (error) {
toast.error(getErrorMessage(error, t("ai.providerSaveFailed")));
} finally {
setSaving(false);
}
}
return (
<div className="space-y-3 rounded-none border border-border p-3">
<div className="space-y-1.5">
<Label htmlFor={`ai-provider-label-${provider.id}`}>
{t("ai.providerLabel")}
</Label>
<Input
id={`ai-provider-label-${provider.id}`}
className="rounded-none"
value={label}
onChange={(event) => setLabel(event.target.value)}
autoFocus
/>
</div>
<div className="space-y-1.5">
<div className="flex items-center gap-2">
<Label
htmlFor={`ai-provider-model-${provider.id}`}
className="min-w-0 flex-1"
>
{t("ai.defaultModel")}
</Label>
<button
type="button"
className="flex shrink-0 items-center gap-1 text-[11px] text-muted-foreground hover:text-foreground disabled:opacity-50"
onClick={() => void detectModels()}
disabled={detecting}
>
{detecting ? (
<Loader2 size={11} className="animate-spin" />
) : (
<RefreshCw size={11} />
)}
{t("ai.modelRefresh")}
</button>
</div>
{models.length > 0 && !customModel ? (
<Select
value={defaultModel || undefined}
onValueChange={(value) => {
if (value === "__custom__") {
setCustomModel(true);
setDefaultModel("");
return;
}
setDefaultModel(value);
}}
>
<SelectTrigger
id={`ai-provider-model-${provider.id}`}
className="rounded-none"
>
<SelectValue placeholder={t("ai.modelPlaceholder")} />
</SelectTrigger>
<SelectContent>
{models.map((model) => (
<SelectItem key={model} value={model}>
{model}
</SelectItem>
))}
<SelectItem value="__custom__">{t("ai.modelCustom")}</SelectItem>
</SelectContent>
</Select>
) : (
<Input
id={`ai-provider-model-${provider.id}`}
className="rounded-none"
value={defaultModel}
onChange={(event) => setDefaultModel(event.target.value)}
placeholder={t("ai.defaultModelPlaceholder")}
/>
)}
{detectWarning && (
<p className="text-[11px] leading-snug text-destructive">
{detectWarning}
</p>
)}
</div>
<div className="flex gap-2">
<Button size="sm" disabled={saving} onClick={() => void handleSave()}>
{saving && <Loader2 size={14} className="animate-spin" />}
{t("ai.save")}
</Button>
<Button
size="sm"
variant="outline"
disabled={saving}
onClick={onCancel}
>
{t("ai.cancel")}
</Button>
</div>
</div>
);
}
export function AiProviderSettings({
providers,
onChanged,
onAdded,
}: AiProviderSettingsProps) {
const { t } = useTranslation();
const [adding, setAdding] = useState(false);
const [editingId, setEditingId] = useState<number | null>(null);
const [saving, setSaving] = useState(false);
const [providerType, setProviderType] = useState<AiProviderType>("ollama");
const [label, setLabel] = useState("");
const [baseUrl, setBaseUrl] = useState("http://localhost:11434");
const [apiKey, setApiKey] = useState("");
const [defaultModel, setDefaultModel] = useState("");
const [models, setModels] = useState<string[]>([]);
const [detecting, setDetecting] = useState(false);
const [detectWarning, setDetectWarning] = useState<string | null>(null);
const [customModel, setCustomModel] = useState(false);
const spec = PROVIDER_TYPES.find((entry) => entry.value === providerType)!;
useEffect(() => {
setBaseUrl(spec.defaultBaseUrl ?? "");
setApiKey("");
setModels([]);
setDefaultModel("");
setCustomModel(false);
setDetectWarning(null);
}, [providerType, spec.defaultBaseUrl]);
/**
* Fetches the provider's own model list so nobody has to go and look model
* names up. Falls back to a curated list when the endpoint is unreachable,
* and a free-text field is always available for anything not listed.
*/
const detectModels = useCallback(async () => {
setDetecting(true);
setDetectWarning(null);
try {
const result = await probeAiModels({
providerType,
baseUrl: baseUrl.trim() || null,
apiKey: apiKey.trim() || null,
});
setModels(result.models);
if (result.source !== "live") {
setDetectWarning(result.warning || t("ai.modelDetectFailed"));
}
// Pick the first suggestion so the field is never left empty.
setDefaultModel((current) => current || result.models[0] || "");
} catch {
setDetectWarning(t("ai.modelDetectFailed"));
} finally {
setDetecting(false);
}
}, [providerType, baseUrl, apiKey, t]);
// Detect as soon as the provider has enough detail to be reachable.
useEffect(() => {
if (!adding) return;
const ready = spec.needsApiKey ? apiKey.trim().length > 0 : true;
if (!ready) return;
const timer = setTimeout(() => void detectModels(), 400);
return () => clearTimeout(timer);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [adding, providerType, baseUrl, apiKey]);
async function handleAdd() {
if (!label.trim()) {
toast.error(t("ai.labelRequired"));
return;
}
setSaving(true);
try {
const created = await createAiProvider({
providerType,
label: label.trim(),
baseUrl: baseUrl.trim() || null,
apiKey: apiKey.trim() || null,
defaultModel: defaultModel.trim() || null,
});
setAdding(false);
setLabel("");
setApiKey("");
setDefaultModel("");
onChanged(created.id);
onAdded?.();
} catch (error) {
toast.error(getErrorMessage(error, t("ai.providerSaveFailed")));
} finally {
setSaving(false);
}
}
async function handleDelete(id: number) {
try {
await deleteAiProvider(id);
onChanged();
} catch (error) {
toast.error(getErrorMessage(error, t("ai.providerDeleteFailed")));
}
}
return (
<div className="space-y-3">
{providers.map((provider) =>
editingId === provider.id ? (
<AiProviderEditForm
key={provider.id}
provider={provider}
onSaved={() => {
setEditingId(null);
onChanged(provider.id);
}}
onCancel={() => setEditingId(null)}
/>
) : (
<div
key={provider.id}
className="flex items-center justify-between gap-2 rounded-none border border-border bg-muted px-3 py-2"
>
<div className="min-w-0">
<div className="truncate text-sm font-medium">
{provider.label}
</div>
<div className="truncate text-xs text-muted-foreground">
{provider.providerType}
{provider.defaultModel ? ` · ${provider.defaultModel}` : ""}
{provider.baseUrl ? ` · ${provider.baseUrl}` : ""}
{provider.apiKeyPrefix ? ` · ${provider.apiKeyPrefix}…` : ""}
</div>
</div>
<div className="flex shrink-0 items-center">
<Button
size="sm"
variant="ghost"
onClick={() => {
setAdding(false);
setEditingId(provider.id);
}}
aria-label={t("ai.editProvider")}
>
<Pencil size={14} />
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => handleDelete(provider.id)}
aria-label={t("ai.removeProvider")}
>
<Trash2 size={14} />
</Button>
</div>
</div>
),
)}
{!adding && (
<Button
size="sm"
variant="outline"
onClick={() => {
setEditingId(null);
setAdding(true);
}}
>
<Plus size={14} />
{t("ai.addProvider")}
</Button>
)}
{adding && (
<div className="space-y-3 rounded-none border border-border p-3">
<div className="space-y-1.5">
<Label>{t("ai.providerType")}</Label>
<Select
value={providerType}
onValueChange={(value) =>
setProviderType(value as AiProviderType)
}
>
<SelectTrigger className="rounded-none">
<SelectValue />
</SelectTrigger>
<SelectContent>
{PROVIDER_TYPES.map((entry) => (
<SelectItem key={entry.value} value={entry.value}>
{t(entry.labelKey)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<Label>{t("ai.providerLabel")}</Label>
<Input
className="rounded-none"
value={label}
onChange={(event) => setLabel(event.target.value)}
placeholder={t("ai.providerLabelPlaceholder")}
/>
</div>
{spec.needsBaseUrl && (
<div className="space-y-1.5">
<Label>{t("ai.baseUrl")}</Label>
<Input
className="rounded-none"
value={baseUrl}
onChange={(event) => setBaseUrl(event.target.value)}
placeholder="http://localhost:11434"
/>
<p className="text-xs text-muted-foreground">
{t("ai.privateEndpointHint")}
</p>
</div>
)}
{(spec.needsApiKey || providerType === "openai_compatible") && (
<div className="space-y-1.5">
<Label>{t("ai.apiKey")}</Label>
<Input
className="rounded-none"
type="password"
value={apiKey}
onChange={(event) => setApiKey(event.target.value)}
autoComplete="off"
/>
</div>
)}
<div className="space-y-1.5">
<div className="flex items-center gap-2">
<Label className="min-w-0 flex-1">{t("ai.defaultModel")}</Label>
<button
type="button"
className="flex shrink-0 items-center gap-1 text-[11px] text-muted-foreground hover:text-foreground"
onClick={() => void detectModels()}
disabled={detecting}
>
{detecting ? (
<Loader2 size={11} className="animate-spin" />
) : (
<RefreshCw size={11} />
)}
{t("ai.modelRefresh")}
</button>
</div>
{models.length > 0 && !customModel ? (
<Select
value={defaultModel || undefined}
onValueChange={(value) => {
if (value === "__custom__") {
setCustomModel(true);
setDefaultModel("");
return;
}
setDefaultModel(value);
}}
>
<SelectTrigger className="rounded-none">
<SelectValue placeholder={t("ai.modelPlaceholder")} />
</SelectTrigger>
<SelectContent>
{models.map((model) => (
<SelectItem key={model} value={model}>
{model}
</SelectItem>
))}
{/* Anything the provider did not list is still reachable. */}
<SelectItem value="__custom__">
{t("ai.modelCustom")}
</SelectItem>
</SelectContent>
</Select>
) : (
<Input
className="rounded-none"
value={defaultModel}
onChange={(event) => setDefaultModel(event.target.value)}
placeholder={t("ai.defaultModelPlaceholder")}
/>
)}
{detectWarning && (
<p className="text-[11px] leading-snug text-muted-foreground">
{detectWarning}
</p>
)}
</div>
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
className="border-accent-brand/40 text-accent-brand hover:bg-accent-brand/10 hover:text-accent-brand"
disabled={saving}
onClick={handleAdd}
>
{saving && <Loader2 size={14} className="animate-spin" />}
{t("ai.save")}
</Button>
<Button
size="sm"
variant="outline"
disabled={saving}
onClick={() => setAdding(false)}
>
{t("ai.cancel")}
</Button>
</div>
</div>
)}
</div>
);
}
+80
View File
@@ -0,0 +1,80 @@
import { useState } from "react";
import { useTranslation } from "@termix/plugin-sdk/frontend";
import { ChevronDown, ChevronRight, Loader2, Wrench } from "lucide-react";
import type { ToolActivity } from "./use-ai-stream";
import { toolLabel } from "./labels";
/**
* Every tool call is shown, so the user can always see what the assistant
* looked at rather than having to trust the summary.
*/
export function AiToolCall({
tool,
streaming = false,
}: {
tool: ToolActivity;
/** False once the turn has ended, whatever ended it. */
streaming?: boolean;
}) {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
// A tool can only still be running while the turn is. An aborted or failed
// stream leaves calls without results, and treating those as pending left a
// spinner running forever after the conversation had finished.
const pending = !("result" in tool) && streaming;
return (
<div className="rounded-none border border-border bg-muted/50 text-xs">
<button
type="button"
className="flex w-full items-center gap-2 px-2 py-1.5 text-left"
onClick={() => setOpen((value) => !value)}
>
{open ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
{pending ? (
<Loader2 size={12} className="animate-spin" />
) : (
<Wrench size={12} />
)}
<span className="truncate">{toolLabel(tool.name)}</span>
{pending && (
<span className="text-muted-foreground">{t("ai.running")}</span>
)}
</button>
{open && (
<div className="space-y-2 border-t border-border px-2 py-2">
{/* The header reads as an action, so the actual tool id lives here. */}
<div className="font-mono text-[10px] text-muted-foreground">
{tool.name}
</div>
{Object.keys(tool.arguments).length > 0 && (
<div>
<div className="mb-1 text-muted-foreground">
{t("ai.toolArguments")}
</div>
<pre className="overflow-x-auto whitespace-pre-wrap break-words font-mono">
{JSON.stringify(tool.arguments, null, 2)}
</pre>
</div>
)}
{/*
Gated on the result existing, not on !pending: an interrupted turn
leaves a call with no result at all, which would otherwise print
"undefined" under a Result heading.
*/}
{"result" in tool && (
<div>
<div className="mb-1 text-muted-foreground">
{t("ai.toolResult")}
</div>
<pre className="max-h-64 overflow-auto whitespace-pre-wrap break-words font-mono">
{JSON.stringify(tool.result, null, 2)}
</pre>
</div>
)}
</div>
)}
</div>
);
}
+208
View File
@@ -0,0 +1,208 @@
import { getErrorMessage } from "@/lib/error-message";
import { useState } from "react";
import { useTranslation } from "@termix/plugin-sdk/frontend";
import { toast } from "sonner";
import { Check, Loader2, TriangleAlert, X } from "lucide-react";
import { Button } from "@/components/button";
import { applyAiProposal, rejectAiProposal, type AiProposal } from "./ai-api";
import { fieldLabel, toolLabel } from "./labels";
interface ProposalCardProps {
proposal: AiProposal;
onResolved: (
id: number,
status: "applied" | "rejected",
resultSummary?: string,
) => void;
}
function isDestructive(kind: string): boolean {
return kind.includes("delete");
}
/**
* Fields shown in their own block rather than the generic row list, so the
* card does not print the same command twice.
*/
const PROMOTED_FIELDS = new Set(["command", "explanation", "reason"]);
/** Renders the payload as readable field rows rather than raw JSON. */
function fieldRows(payload: Record<string, unknown>): Array<[string, string]> {
const rows: Array<[string, string]> = [];
const push = (key: string, value: unknown) => {
if (value === null || value === undefined || value === "") return;
if (PROMOTED_FIELDS.has(key)) return;
const text =
typeof value === "object" ? JSON.stringify(value) : String(value);
rows.push([key, text]);
};
for (const [key, value] of Object.entries(payload)) {
if (key === "changes" && value && typeof value === "object") {
for (const [childKey, childValue] of Object.entries(
value as Record<string, unknown>,
)) {
push(childKey, childValue);
}
continue;
}
push(key, value);
}
return rows;
}
function asText(value: unknown): string | null {
return typeof value === "string" && value.trim() ? value.trim() : null;
}
export function ProposalCard({ proposal, onResolved }: ProposalCardProps) {
const { t } = useTranslation();
const [busy, setBusy] = useState<"apply" | "reject" | null>(null);
let payload: Record<string, unknown> = {};
try {
payload = JSON.parse(proposal.payload);
} catch {
payload = {};
}
const rows = fieldRows(payload);
const destructive = isDestructive(proposal.kind);
const resolved = proposal.status !== "pending";
const command = asText(payload.command);
const explanation = asText(payload.explanation) ?? asText(payload.reason);
// The stored summary repeats the command for run_command proposals, so a
// short action label is used instead of printing it twice.
const title =
command !== null
? t("ai.runCommandTitle")
: (proposal.summary ?? toolLabel(proposal.kind));
async function handleApply() {
setBusy("apply");
try {
const result = await applyAiProposal(proposal.id);
// The result goes into the card, not a toast: command output can be
// hundreds of lines, which covered the screen.
onResolved(proposal.id, "applied", result.summary);
} catch (error) {
toast.error(getErrorMessage(error, t("ai.proposalApplyFailed")));
} finally {
setBusy(null);
}
}
async function handleReject() {
setBusy("reject");
try {
await rejectAiProposal(proposal.id);
onResolved(proposal.id, "rejected");
} catch (error) {
toast.error(getErrorMessage(error, t("ai.proposalRejectFailed")));
} finally {
setBusy(null);
}
}
return (
<div className="rounded-none border border-border bg-muted p-2.5">
<div className="flex items-center gap-1.5">
{destructive && (
<TriangleAlert size={13} className="shrink-0 text-destructive" />
)}
<span className="min-w-0 flex-1 truncate text-xs font-medium">
{title}
</span>
{resolved && (
<span className="shrink-0 border border-border px-1.5 py-0.5 text-[10px] text-muted-foreground">
{proposal.status === "applied"
? t("ai.statusApplied")
: t("ai.statusRejected")}
</span>
)}
</div>
{/*
A command is the payload, not a field of it, so it gets its own block
rather than a label column. In the sidebar a fixed label gutter left
barely any room for the command itself.
*/}
{command && (
<pre className="mt-2 max-h-40 overflow-auto whitespace-pre-wrap break-all border border-border bg-background px-2 py-1.5 font-mono text-[11px] leading-snug">
{command}
</pre>
)}
{explanation && (
<p className="mt-1.5 text-[11px] leading-snug text-muted-foreground">
{explanation}
</p>
)}
{rows.length > 0 && (
<div className="mt-2 space-y-1.5 border-t border-border pt-2">
{rows.map(([key, value]) => (
// Stacked, not a two-column row: the value is what matters and it
// needs the full width to stay readable.
<div key={key} className="min-w-0">
<span className="text-[10px] text-muted-foreground">
{fieldLabel(key)}
</span>
<div className="break-all font-mono text-[11px] leading-snug">
{value}
</div>
</div>
))}
</div>
)}
{proposal.resultSummary && (
// Command output comes back here, so it keeps the monospace treatment.
<pre className="mt-2 max-h-40 overflow-auto whitespace-pre-wrap break-all border border-border bg-background px-2 py-1.5 font-mono text-[11px] leading-snug text-muted-foreground">
{proposal.resultSummary}
</pre>
)}
{!resolved && (
<div className="mt-2 flex gap-1.5">
<Button
size="sm"
className={`h-7 flex-1 text-xs ${
destructive
? ""
: "border-accent-brand/40 text-accent-brand hover:bg-accent-brand/10 hover:text-accent-brand"
}`}
variant={destructive ? "destructive" : "outline"}
disabled={busy !== null}
onClick={handleApply}
>
{busy === "apply" ? (
<Loader2 size={13} className="animate-spin" />
) : (
<Check size={13} />
)}
{t("ai.approve")}
</Button>
<Button
size="sm"
className="h-7 flex-1 text-xs"
variant="outline"
disabled={busy !== null}
onClick={handleReject}
>
{busy === "reject" ? (
<Loader2 size={13} className="animate-spin" />
) : (
<X size={13} />
)}
{t("ai.reject")}
</Button>
</div>
)}
</div>
);
}
+247
View File
@@ -0,0 +1,247 @@
import { authApi, handleApiError } from "@/main-axios";
export type AiProviderType =
"ollama" | "anthropic" | "openai" | "gemini" | "openai_compatible";
export interface AiProvider {
id: number;
providerType: AiProviderType;
label: string;
baseUrl: string | null;
/** The first few characters only; the key itself never leaves the server. */
apiKeyPrefix: string | null;
defaultModel: string | null;
enabled: boolean;
createdAt: string;
}
export interface AiConversation {
id: number;
title: string | null;
providerId: number | null;
model: string | null;
createdAt: string;
updatedAt: string;
}
export interface AiMessage {
id: number;
conversationId: number;
role: "user" | "assistant" | "tool";
content: string;
toolCalls: string | null;
createdAt: string;
}
export interface AiProposal {
id: number;
conversationId: number;
kind: string;
summary: string | null;
payload: string;
status: "pending" | "applied" | "rejected" | "expired";
resultSummary: string | null;
createdAt: string;
}
export interface AiStatus {
globallyEnabled: boolean;
enabled: boolean;
allowReadOnlyCommands: boolean;
}
export async function getAiStatus(): Promise<AiStatus> {
try {
return (await authApi.get("/plugin-api/ai/status")).data;
} catch (error) {
throw handleApiError(error, "get AI status");
}
}
export async function getAiProviders(): Promise<AiProvider[]> {
try {
return (await authApi.get("/plugin-api/ai/providers")).data.providers;
} catch (error) {
throw handleApiError(error, "list AI providers");
}
}
export async function createAiProvider(input: {
providerType: AiProviderType;
label: string;
baseUrl?: string | null;
apiKey?: string | null;
defaultModel?: string | null;
}): Promise<AiProvider> {
try {
return (await authApi.post("/plugin-api/ai/providers", input)).data
.provider;
} catch (error) {
throw handleApiError(error, "create AI provider");
}
}
export async function updateAiProvider(
id: number,
input: Partial<{
label: string;
baseUrl: string | null;
apiKey: string | null;
defaultModel: string | null;
enabled: boolean;
}>,
): Promise<AiProvider> {
try {
return (await authApi.patch(`/plugin-api/ai/providers/${id}`, input)).data
.provider;
} catch (error) {
throw handleApiError(error, "update AI provider");
}
}
export async function deleteAiProvider(id: number): Promise<void> {
try {
await authApi.delete(`/plugin-api/ai/providers/${id}`);
} catch (error) {
throw handleApiError(error, "delete AI provider");
}
}
/**
* Model list for a provider that may not be saved yet, so the add form can
* fill its picker before anything is persisted.
*/
export async function probeAiModels(input: {
providerType: AiProviderType;
baseUrl?: string | null;
apiKey?: string | null;
providerId?: number | null;
}): Promise<{
models: string[];
source: "live" | "fallback" | "none";
warning?: string;
}> {
try {
return (await authApi.post("/plugin-api/ai/probe-models", input)).data;
} catch (error) {
throw handleApiError(error, "detect models");
}
}
export async function getAiProviderModels(id: number): Promise<string[]> {
try {
return (await authApi.get(`/plugin-api/ai/providers/${id}/models`)).data
.models;
} catch (error) {
throw handleApiError(error, "list provider models");
}
}
export async function getAiConversations(): Promise<AiConversation[]> {
try {
return (await authApi.get("/plugin-api/ai/conversations")).data
.conversations;
} catch (error) {
throw handleApiError(error, "list AI conversations");
}
}
export async function getAiConversation(id: number): Promise<{
conversation: AiConversation;
messages: AiMessage[];
proposals: AiProposal[];
}> {
try {
return (await authApi.get(`/plugin-api/ai/conversations/${id}`)).data;
} catch (error) {
throw handleApiError(error, "load AI conversation");
}
}
export async function deleteAiConversation(id: number): Promise<void> {
try {
await authApi.delete(`/plugin-api/ai/conversations/${id}`);
} catch (error) {
throw handleApiError(error, "delete AI conversation");
}
}
export async function applyAiProposal(
id: number,
): Promise<{ success: boolean; summary: string }> {
try {
return (await authApi.post(`/plugin-api/ai/proposals/${id}/apply`)).data;
} catch (error) {
throw handleApiError(error, "apply AI proposal");
}
}
export async function rejectAiProposal(id: number): Promise<void> {
try {
await authApi.post(`/plugin-api/ai/proposals/${id}/reject`);
} catch (error) {
throw handleApiError(error, "reject AI proposal");
}
}
/**
* For the terminal-docked assistant: records that a run_command proposal was
* typed into the user's open terminal instead of run over a pooled connection.
*/
export async function markAiProposalRunInTerminal(
id: number,
hostId: number,
summary?: string,
): Promise<{ success: boolean; summary: string }> {
try {
return (
await authApi.post(
`/plugin-api/ai/proposals/${id}/mark-run-in-terminal`,
{
hostId,
summary,
},
)
).data;
} catch (error) {
throw handleApiError(error, "mark AI proposal run in terminal");
}
}
// --- admin ---
export async function getAiGloballyEnabled(): Promise<boolean> {
try {
return (await authApi.get("/plugin-api/ai/enabled")).data.enabled;
} catch (error) {
throw handleApiError(error, "get AI enabled setting");
}
}
export async function setAiGloballyEnabled(enabled: boolean): Promise<boolean> {
try {
return (await authApi.patch("/plugin-api/ai/enabled", { enabled })).data
.enabled;
} catch (error) {
throw handleApiError(error, "update AI enabled setting");
}
}
export async function getAiPrivateEndpoints(): Promise<string[]> {
try {
return (await authApi.get("/plugin-api/ai/private-endpoints")).data.hosts;
} catch (error) {
throw handleApiError(error, "get AI endpoint allowlist");
}
}
export async function setAiPrivateEndpoints(
hosts: string[],
): Promise<string[]> {
try {
return (await authApi.patch("/plugin-api/ai/private-endpoints", { hosts }))
.data.hosts;
} catch (error) {
throw handleApiError(error, "update AI endpoint allowlist");
}
}
-68
View File
@@ -1,68 +0,0 @@
/**
* Frontend half of the ai plugin.
*
* AiPanel and the rest of src/ui/features/ai/ stay in core rather than
* moving here: TerminalAiPanel.tsx and TerminalProposalCard.tsx in
* src/ui/features/terminal/ (also core) import AiMessage, AiToolCall,
* useAiStream and ProposalCard directly, for the terminal's own docked AI
* panel. That is a real external dependency, the same reason session-manager
* stays in core for ssh-terminal rather than being copied into that plugin.
*
* The "ai" surface itself is not registered through registerTabComponent:
* it is a rail-view/right-dock panel baked into AppShell.tsx and
* tabUtils.tsx (railView === "ai", case "ai"), the same way the terminal's
* tab is baked into Terminal.tsx rather than going through the generic tab
* registry. What gates it is isTabTypeAvailable("ai") in
* src/ui/shell/pluginLoader.ts's BUILT_IN_TABS_BY_PLUGIN map.
*
* What this file DOES contribute is the terminal toolbar's AI button, through
* the action registry: ai.openWithContext, declared in manifest.json and
* gated on ai.services.use. The button used to be hardwired into
* TerminalToolbar.tsx behind a showAiAssistant prop.
*
* There is still no frontend plugin loader, so nothing imports this file yet
* (see src/ui/tests/sidebar/plugin-extension-seam.test.tsx). Until one exists,
* pluginLoader.ts performs the same registration itself when the ai plugin is
* enabled. This file is the version that runs once a loader lands, and the two
* must be kept in step.
*/
export const id = "ai";
export const tabId = "ai";
export const actionId = "ai.openWithContext";
/** Asks the focused terminal to open its AI panel seeded with `context`. */
export function openWithContext(context) {
window.dispatchEvent(
new CustomEvent("termix:ai:openWithContext", {
detail: { context: typeof context === "string" ? context : "" },
}),
);
}
export async function register({
registerAction,
registerSlotContribution,
icons,
}) {
registerAction(actionId, openWithContext, {
permission: "ai.services.use",
pluginId: id,
});
registerSlotContribution("terminal.toolbar", {
actionId,
titleKey: "ai.assistant",
icon: icons.Bot,
kind: "button",
pluginId: id,
});
}
export async function unregister({
unregisterAction,
unregisterSlotContribution,
}) {
unregisterSlotContribution("terminal.toolbar", actionId);
unregisterAction(actionId);
}
+183
View File
@@ -0,0 +1,183 @@
import type { ComponentType } from "react";
import { Bot, Sparkles } from "lucide-react";
import type {
PanelProps,
TabProps,
TermixApp,
} from "@termix/plugin-sdk/frontend";
import type {
TerminalDockProps,
TerminalSlotApi,
} from "@/features/terminal/terminal-slots";
import { AiPanel } from "./AiPanel";
import { TerminalAiPanel } from "./terminal/TerminalAiPanel";
import { AiAssistantStep } from "./AiAssistantStep";
import { AiAccessSettings } from "./settings/AiAccessSettings";
import { AI_RAIL_ID, AiUserSettings } from "./settings/AiUserSettings";
import { getAiStatus } from "./ai-api";
import { AI_STATUS_CHANGED_EVENT } from "./use-ai-availability";
const ASSISTANT_ACTION = "ai.openWithContext";
const DOCK_ID = "ai.assistant";
function AssistantPanel({ activeTabType }: PanelProps) {
return (
<div className="flex flex-col flex-1 min-h-0">
<AiPanel activeTab={activeTabType ?? null} />
</div>
);
}
function AssistantTab() {
return <AiPanel />;
}
/** The assistant docked beside a terminal. */
function TerminalDock({
host,
hostId,
hostLabel,
dockProps,
onClose,
onRunInTerminal,
}: TerminalDockProps) {
if (!hostId) return null;
return (
<TerminalAiPanel
hostLabel={hostLabel}
hostId={hostId}
activeTab={`terminal:${host?.name || host?.ip || ""}`}
initialContext={
typeof dockProps.context === "string" ? dockProps.context : ""
}
onClose={onClose}
onRunInTerminal={onRunInTerminal}
/>
);
}
export function activate(app: TermixApp): void {
let globallyEnabled = false;
let userEnabled = false;
const surface: (() => void)[] = [];
const clearSurface = () => {
while (surface.length > 0) surface.pop()!();
};
// What the assistant shows depends on the admin switch and the user's own
// choice, so it is re-registered whenever either changes.
const applyStatus = () => {
clearSurface();
surface.push(
app.registerRailItem({
id: AI_RAIL_ID,
icon: Sparkles,
titleKey: "nav.ai",
promotable: true,
rightDockable: true,
after: "macros",
order: 40,
// Off for everyone: not even a Navigation toggle.
hidden: !globallyEnabled,
}),
);
if (!globallyEnabled) {
app.tabs.closeTab(AI_RAIL_ID);
return;
}
surface.push(
app.registerTab(AI_RAIL_ID, AssistantTab as ComponentType<TabProps>, {
icon: Sparkles,
titleKey: "nav.ai",
singleton: true,
hostless: true,
panelFrame: true,
}),
);
const offeredOnHost = (context: Record<string, unknown>) =>
userEnabled &&
(context.host as { enableAiAssistant?: boolean } | undefined)
?.enableAiAssistant === true;
surface.push(
app.registerSlotContribution("terminal.toolbar", {
actionId: ASSISTANT_ACTION,
titleKey: "ai.assistant",
icon: Bot,
kind: "button",
when: offeredOnHost,
}),
);
surface.push(
app.registerSlotContribution("terminal.dock", {
actionId: DOCK_ID,
titleKey: "ai.assistant",
icon: Bot,
kind: "component",
component: TerminalDock as unknown as ComponentType<
Record<string, unknown>
>,
when: offeredOnHost,
}),
);
surface.push(
app.registerSlotContribution("onboarding.steps", {
actionId: "ai.onboarding",
titleKey: "onboarding.aiTitle",
kind: "component",
component: AiAssistantStep as unknown as ComponentType<
Record<string, unknown>
>,
}),
);
};
const refresh = () => {
getAiStatus()
.then((status) => {
globallyEnabled = status.globallyEnabled;
userEnabled = status.globallyEnabled && status.enabled;
})
.catch(() => {
globallyEnabled = false;
userEnabled = false;
})
.finally(() => {
if (!disposed) applyStatus();
});
};
let disposed = false;
app.onDispose(() => {
disposed = true;
clearSurface();
});
app.registerPanel(AI_RAIL_ID, AssistantPanel);
// Opens the docked assistant on the terminal it was clicked from, seeded
// with what is on screen.
app.registerAction(
ASSISTANT_ACTION,
(terminal: TerminalSlotApi) =>
terminal?.openDock(DOCK_ID, { context: terminal.getBufferText() }),
{ permission: "services.use" },
);
app.registerSettingsComponent("access", AiAccessSettings);
app.registerSettingsComponent("assistant", AiUserSettings);
applyStatus();
refresh();
window.addEventListener(AI_STATUS_CHANGED_EVENT, refresh);
window.addEventListener("hiddenRailTabsChanged", refresh);
app.onDispose(() => {
window.removeEventListener(AI_STATUS_CHANGED_EVENT, refresh);
window.removeEventListener("hiddenRailTabsChanged", refresh);
});
}
+84
View File
@@ -0,0 +1,84 @@
/**
* Turns the internal identifiers the assistant works in into text a user can
* read. Tool names, proposal kinds and payload keys all surface in the panel,
* and raw ids like "propose_create_automation" or "thresholddurationseconds"
* are not something to show anyone.
*/
/** What each tool is doing, phrased as an action rather than a function name. */
const TOOL_LABELS: Record<string, string> = {
list_hosts: "Reading hosts",
get_host: "Reading host",
list_fleets: "Reading fleets",
list_snippets: "Reading snippets",
list_automations: "Reading automations",
get_automation: "Reading automation",
list_workspaces: "Reading workspaces",
list_notification_channels: "Reading notification channels",
list_homepage_items: "Reading homepage items",
get_command_history: "Reading command history",
get_network_topology: "Reading network topology",
propose_create_host: "Add host",
propose_update_host: "Update host",
propose_delete_host: "Delete host",
propose_create_snippet: "Create snippet",
propose_update_snippet: "Update snippet",
propose_delete_snippet: "Delete snippet",
propose_create_automation: "Create automation",
propose_create_fleet: "Create fleet",
propose_run_command: "Run command",
};
/** Readable names for the payload keys the propose tools emit. */
const FIELD_LABELS: Record<string, string> = {
hostId: "Host",
hostIds: "Hosts",
snippetId: "Snippet",
automationId: "Automation",
name: "Name",
ip: "Address",
port: "Port",
username: "Username",
folder: "Folder",
tags: "Tags",
content: "Command",
description: "Description",
definition: "Definition",
enabled: "Enabled",
triggerType: "Trigger",
thresholdValue: "Threshold",
thresholdDurationSeconds: "Duration (seconds)",
cooldownMinutes: "Cooldown (minutes)",
};
const MENTION_LABELS: Record<string, string> = {
host: "Host",
snippet: "Snippet",
automation: "Automation",
};
/**
* Splits snake_case and camelCase into words and capitalises the first.
* Used whenever an identifier has no explicit label, so something added later
* still reads as words rather than one run-on token.
*/
export function humanize(value: string): string {
const spaced = value
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
.replace(/[_-]+/g, " ")
.trim();
if (!spaced) return value;
return spaced.charAt(0).toUpperCase() + spaced.slice(1).toLowerCase();
}
export function toolLabel(name: string): string {
return TOOL_LABELS[name] ?? humanize(name);
}
export function fieldLabel(key: string): string {
return FIELD_LABELS[key] ?? humanize(key);
}
export function mentionLabel(kind: string): string {
return MENTION_LABELS[kind] ?? humanize(kind);
}
@@ -0,0 +1,95 @@
import { useEffect, useState } from "react";
import { useToast, useTranslation } from "@termix/plugin-sdk/frontend";
import { FakeSwitch, Input, SettingRow } from "@termix/plugin-sdk/ui";
import {
getAiGloballyEnabled,
getAiPrivateEndpoints,
setAiGloballyEnabled,
setAiPrivateEndpoints,
} from "../ai-api";
import { notifyAiStatusChanged } from "../use-ai-availability";
/**
* The instance-wide switch and the private address allowlist. Stored in the
* assistant's own backend rather than plugin settings, so this saves on
* change instead of through the page's Save.
*/
export function AiAccessSettings() {
const { t } = useTranslation();
const toast = useToast();
const [enabled, setEnabled] = useState(false);
const [endpoints, setEndpoints] = useState<string[] | null>(null);
useEffect(() => {
let cancelled = false;
void Promise.allSettled([
getAiGloballyEnabled(),
getAiPrivateEndpoints(),
]).then(([enabledResult, endpointsResult]) => {
if (cancelled) return;
if (enabledResult.status === "fulfilled") setEnabled(enabledResult.value);
setEndpoints(
endpointsResult.status === "fulfilled" ? endpointsResult.value : [],
);
});
return () => {
cancelled = true;
};
}, []);
const toggle = async (next: boolean) => {
setEnabled(next);
try {
await setAiGloballyEnabled(next);
notifyAiStatusChanged();
} catch {
setEnabled(!next);
toast.error(t("admin.updateAiEnabledFailed"));
}
};
const saveEndpoints = async (hosts: string[]) => {
const previous = endpoints ?? [];
setEndpoints(hosts);
try {
setEndpoints(await setAiPrivateEndpoints(hosts));
} catch {
setEndpoints(previous);
toast.error(t("admin.updateAiEndpointsFailed"));
}
};
return (
<div className="flex flex-col gap-1">
<SettingRow
label={t("admin.aiGloballyEnabled")}
description={t("admin.aiGloballyEnabledDesc")}
>
<FakeSwitch checked={enabled} onChange={(next) => void toggle(next)} />
</SettingRow>
{enabled && endpoints !== null && (
<div className="flex flex-col gap-1.5 py-2">
<span className="text-xs font-medium">
{t("admin.aiPrivateEndpoints")}
</span>
<span className="text-[11px] leading-snug text-muted-foreground">
{t("admin.aiPrivateEndpointsDesc")}
</span>
<Input
className="rounded-none"
defaultValue={endpoints.join(", ")}
placeholder="localhost, 127.0.0.1"
onBlur={(event) =>
void saveEndpoints(
event.target.value
.split(",")
.map((entry) => entry.trim())
.filter(Boolean),
)
}
/>
</div>
)}
</div>
);
}
@@ -0,0 +1,112 @@
import { useEffect, useState } from "react";
import { useTranslation } from "@termix/plugin-sdk/frontend";
import { FakeSwitch, SettingRow } from "@termix/plugin-sdk/ui";
import { saveUserPreferences } from "@/main-axios";
import { readHiddenRailTabs } from "@/sidebar/hidden-rail-tabs";
import { getAiStatus } from "../ai-api";
import { notifyAiStatusChanged } from "../use-ai-availability";
/** The rail id the assistant registers under. */
export const AI_RAIL_ID = "ai";
/**
* Turns the assistant on or off for one person. Off also hides its rail
* entry, so declining removes the feature from view rather than leaving an
* inert button behind. Stored in core user preferences, where it has always
* lived, so it roams with the rest of a user's settings.
*/
export function setAssistantEnabledForUser(enabled: boolean): void {
const hidden = readHiddenRailTabs();
if (enabled) hidden.delete(AI_RAIL_ID);
else hidden.add(AI_RAIL_ID);
const serialized = JSON.stringify([...hidden]);
localStorage.setItem("hiddenRailTabs", serialized);
window.dispatchEvent(new Event("hiddenRailTabsChanged"));
saveUserPreferences({
aiAssistantEnabled: enabled,
hiddenRailTabs: serialized,
})
.catch(() => {
// The local choice still applies if the write fails.
})
.finally(() => notifyAiStatusChanged());
}
export function AiUserSettings() {
const { t } = useTranslation();
const [status, setStatus] = useState<{
globallyEnabled: boolean;
enabled: boolean;
readOnly: boolean;
} | null>(null);
useEffect(() => {
let cancelled = false;
getAiStatus()
.then((next) => {
if (!cancelled) {
setStatus({
globallyEnabled: next.globallyEnabled,
enabled: next.enabled,
readOnly: next.allowReadOnlyCommands,
});
}
})
.catch(() => {
if (!cancelled) {
setStatus({
globallyEnabled: false,
enabled: false,
readOnly: false,
});
}
});
return () => {
cancelled = true;
};
}, []);
// A user who cannot have the feature is not told about it.
if (!status?.globallyEnabled) {
return (
<p className="text-xs text-muted-foreground">
{t("settings.assistantUnavailable")}
</p>
);
}
return (
<div className="flex flex-col gap-1">
<SettingRow
label={t("ai.enableTitle")}
description={t("ai.enableDescription")}
>
<FakeSwitch
checked={status.enabled}
onChange={(enabled) => {
setStatus({ ...status, enabled });
setAssistantEnabledForUser(enabled);
}}
/>
</SettingRow>
{status.enabled && (
<SettingRow
label={t("ai.readOnlyCommands")}
description={t("ai.readOnlyCommandsDescription")}
>
<FakeSwitch
checked={status.readOnly}
onChange={(readOnly) => {
setStatus({ ...status, readOnly });
saveUserPreferences({ aiReadOnlyCommands: readOnly }).catch(
() => {},
);
}}
/>
</SettingRow>
)}
</div>
);
}
@@ -0,0 +1,419 @@
import {
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState,
} from "react";
import { useTranslation } from "@termix/plugin-sdk/frontend";
import { Bot, GripVertical, Send, Square, X } from "lucide-react";
import { Button } from "@/components/button";
import { getAiProviders, type AiProposal, type AiProvider } from "../ai-api";
import { AiMessage } from "../AiMessage";
import { AiToolCall } from "../AiToolCall";
import { useAiStream } from "../use-ai-stream";
import { TerminalProposalCard } from "./TerminalProposalCard";
import {
clampToolbarPosition,
persistAiPanelPosition,
readStoredAiPanelPosition,
type ToolbarPosition,
} from "@/features/terminal/toolbar-geometry";
interface HistoryEntry {
role: "user" | "assistant";
content: string;
}
type TimelineItem =
| {
kind: "message";
key: string;
role: "user" | "assistant";
content: string;
}
| {
kind: "tool";
key: string;
tool: import("../use-ai-stream").ToolActivity;
}
| { kind: "proposal"; key: string; proposal: AiProposal };
interface TerminalAiPanelProps {
hostLabel: string;
hostId: number;
activeTab?: string | null;
/** Terminal output to open with, from a contributed action. */
initialContext?: string;
onClose: () => void;
onRunInTerminal: (command: string) => void;
}
/**
* A slimmed-down version of AiPanel docked to a terminal tab. Same backend
* (/ai/chat/stream, propose_run_command, ProposalCard) as the main assistant,
* so it respects the same admin/user gates and never runs a command without
* an explicit approve.
*/
export function TerminalAiPanel({
hostLabel,
hostId,
activeTab,
initialContext,
onClose,
onRunInTerminal,
}: TerminalAiPanelProps) {
const { t } = useTranslation();
const { state, send, stop } = useAiStream();
const [providers, setProviders] = useState<AiProvider[]>([]);
const [providerId, setProviderId] = useState<number | null>(null);
const [loading, setLoading] = useState(true);
const [input, setInput] = useState("");
const [history, setHistory] = useState<HistoryEntry[]>([]);
const [resolvedProposals, setResolvedProposals] = useState<
Record<number, { status: "applied" | "rejected"; resultSummary?: string }>
>({});
const conversationIdRef = useRef<number | null>(null);
const scrollRef = useRef<HTMLDivElement>(null);
// Prefilled rather than sent: the user sees exactly what the terminal is
// about to hand the model, and can trim it or add a question first.
useEffect(() => {
if (!initialContext) return;
setInput((current) =>
current
? current
: t("ai.terminalContextPrefill", { output: initialContext }),
);
}, [initialContext, t]);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const [position, setPosition] = useState<ToolbarPosition>(
readStoredAiPanelPosition,
);
const positionRef = useRef(position);
positionRef.current = position;
const panelRef = useRef<HTMLDivElement>(null);
const dragRef = useRef<{
pointerId: number;
startX: number;
startY: number;
basePosition: ToolbarPosition;
} | null>(null);
useLayoutEffect(() => {
const panel = panelRef.current;
const host = panel?.offsetParent as HTMLElement | null;
if (!panel || !host) return;
const next = clampToolbarPosition(
positionRef.current,
panel.getBoundingClientRect(),
host.getBoundingClientRect(),
positionRef.current,
);
if (next.x !== positionRef.current.x || next.y !== positionRef.current.y) {
positionRef.current = next;
setPosition(next);
}
// Only re-clamp when the panel first mounts; drag handles the rest.
}, []);
const handleDragPointerDown = (event: React.PointerEvent<HTMLDivElement>) => {
if (event.button !== 0 || event.pointerType === "touch") return;
if ((event.target as HTMLElement).closest("button")) return;
event.currentTarget.setPointerCapture?.(event.pointerId);
dragRef.current = {
pointerId: event.pointerId,
startX: event.clientX,
startY: event.clientY,
basePosition: positionRef.current,
};
};
const handleDragPointerMove = (event: React.PointerEvent<HTMLDivElement>) => {
const drag = dragRef.current;
if (!drag || drag.pointerId !== event.pointerId) return;
const panel = panelRef.current;
const host = panel?.offsetParent as HTMLElement | null;
if (!panel || !host) return;
const deltaX = event.clientX - drag.startX;
const deltaY = event.clientY - drag.startY;
const next = clampToolbarPosition(
{ x: drag.basePosition.x + deltaX, y: drag.basePosition.y + deltaY },
panel.getBoundingClientRect(),
host.getBoundingClientRect(),
positionRef.current,
);
positionRef.current = next;
setPosition(next);
};
const handleDragPointerEnd = (event: React.PointerEvent<HTMLDivElement>) => {
const drag = dragRef.current;
if (!drag || drag.pointerId !== event.pointerId) return;
persistAiPanelPosition(positionRef.current);
dragRef.current = null;
if (event.type !== "lostpointercapture") {
try {
event.currentTarget.releasePointerCapture?.(event.pointerId);
} catch {
// The browser may have already released capture for this pointer.
}
}
};
useEffect(() => {
let cancelled = false;
getAiProviders()
.then((list) => {
if (cancelled) return;
setProviders(list);
setProviderId((current) => current ?? list[0]?.id ?? null);
})
.catch(() => {
if (!cancelled) setProviders([]);
})
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => {
cancelled = true;
};
}, []);
useEffect(() => {
window.requestAnimationFrame(() => textareaRef.current?.focus());
}, []);
useEffect(() => {
scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight });
}, [state.assistantText, state.tools.length, history.length]);
const handleResolved = useCallback(
(id: number, status: "applied" | "rejected", resultSummary?: string) => {
setResolvedProposals((current) => ({
...current,
[id]: { status, resultSummary },
}));
},
[],
);
const handleSend = useCallback(() => {
const message = input.trim();
if (!message || !providerId || state.streaming) return;
setHistory((current) => [...current, { role: "user", content: message }]);
setInput("");
void send({
message,
providerId,
conversationId: conversationIdRef.current,
activeTab: activeTab ?? `terminal:${hostLabel}`,
onComplete: (conversationId, reply) => {
conversationIdRef.current = conversationId;
if (reply) {
setHistory((current) => [
...current,
{ role: "assistant", content: reply },
]);
}
},
});
}, [activeTab, hostLabel, input, providerId, send, state.streaming]);
const proposals: AiProposal[] = state.proposals.map((proposal) => {
const resolved = resolvedProposals[proposal.id];
if (!resolved) return proposal;
return {
...proposal,
status: resolved.status,
resultSummary: resolved.resultSummary ?? proposal.resultSummary,
};
});
// The reply exists in two places once a turn finishes: the streamed text in
// `state`, and the copy `onComplete` appended to `history`. Show it once,
// and keep tool calls spliced in ahead of the reply they produced instead
// of in a separate trailing block, so the timeline reads in the order
// things actually happened rather than text-then-tools-then-proposals.
const last = history[history.length - 1];
const historyEndsWithThisReply =
state.assistantText.length > 0 &&
last?.role === "assistant" &&
last.content === state.assistantText;
const streamingReply = historyEndsWithThisReply ? "" : state.assistantText;
const trailingReply = historyEndsWithThisReply ? history.length - 1 : -1;
const timeline: TimelineItem[] = [];
history.forEach((entry, index) => {
if (index === trailingReply) return;
timeline.push({
kind: "message",
key: `history-${index}`,
role: entry.role,
content: entry.content,
});
});
for (const tool of state.tools) {
timeline.push({ kind: "tool", key: tool.id, tool });
}
if (trailingReply >= 0) {
timeline.push({
kind: "message",
key: `history-${trailingReply}`,
role: "assistant",
content: history[trailingReply].content,
});
} else if (streamingReply) {
timeline.push({
kind: "message",
key: "streaming",
role: "assistant",
content: streamingReply,
});
}
for (const proposal of proposals) {
timeline.push({
kind: "proposal",
key: `proposal-${proposal.id}`,
proposal,
});
}
return (
<div
ref={panelRef}
className="absolute right-2 top-12 z-[120] flex max-h-[calc(100%-4rem)] w-[min(420px,calc(100vw-1rem))] flex-col border border-border bg-background shadow-xl"
style={{ transform: `translate(${position.x}px, ${position.y}px)` }}
onClick={(event) => event.stopPropagation()}
>
<div
className="flex cursor-grab items-center justify-between gap-2 border-b border-border px-3 py-2 active:cursor-grabbing"
onPointerDown={handleDragPointerDown}
onPointerMove={handleDragPointerMove}
onPointerUp={handleDragPointerEnd}
onPointerCancel={handleDragPointerEnd}
onLostPointerCapture={handleDragPointerEnd}
>
<div className="flex min-w-0 items-center gap-2">
<GripVertical className="size-3.5 shrink-0 text-muted-foreground" />
<Bot className="size-4 shrink-0 text-accent-brand" />
<div className="min-w-0">
<div className="truncate text-xs font-bold text-foreground">
{t("ai.assistant")}
</div>
<div className="truncate text-[10px] text-muted-foreground">
{hostLabel}
</div>
</div>
</div>
<div className="flex items-center gap-1">
<Button
type="button"
size="icon"
variant="ghost"
className="size-7"
onClick={stop}
disabled={!state.streaming}
title={t("ai.stop")}
>
<Square className="size-3.5" />
</Button>
<Button
type="button"
size="icon"
variant="ghost"
className="size-7"
onClick={onClose}
title={t("common.close")}
>
<X className="size-3.5" />
</Button>
</div>
</div>
<div
ref={scrollRef}
className="flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto p-3"
>
{timeline.length === 0 && !loading && (
<div className="text-xs text-muted-foreground">
{t("ai.terminalPanelEmpty")}
</div>
)}
{!loading && providers.length === 0 && (
<div className="border border-border bg-muted/30 px-2 py-1.5 text-xs text-muted-foreground">
{t("ai.noProvidersConfigured")}
</div>
)}
{state.error && (
<div className="border border-destructive/30 bg-destructive/10 px-2 py-1.5 text-xs text-destructive">
{state.error}
</div>
)}
{timeline.map((item) => {
if (item.kind === "message") {
return (
<AiMessage
key={item.key}
role={item.role}
content={item.content}
/>
);
}
if (item.kind === "tool") {
return (
<AiToolCall
key={item.key}
tool={item.tool}
streaming={state.streaming}
/>
);
}
return (
<TerminalProposalCard
key={item.key}
proposal={item.proposal}
hostId={hostId}
onRunInTerminal={onRunInTerminal}
onResolved={handleResolved}
/>
);
})}
</div>
<div className="flex flex-col gap-2 border-t border-border p-3">
<textarea
ref={textareaRef}
value={input}
onChange={(event) => setInput(event.target.value)}
onKeyDown={(event) => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
handleSend();
}
}}
placeholder={t("ai.terminalPanelPlaceholder")}
rows={3}
className="min-h-20 resize-none border border-input bg-background px-2 py-1.5 text-xs text-foreground outline-none focus-visible:ring-1 focus-visible:ring-ring"
disabled={state.streaming || providers.length === 0}
/>
<Button
type="button"
size="sm"
className="h-8 gap-1.5"
disabled={state.streaming || !input.trim() || !providerId}
onClick={handleSend}
>
<Send className="size-3.5" />
{t("ai.send")}
</Button>
</div>
</div>
);
}
@@ -0,0 +1,142 @@
import { getErrorMessage } from "@/lib/error-message";
import { useState } from "react";
import { useTranslation } from "@termix/plugin-sdk/frontend";
import { toast } from "sonner";
import { Check, Loader2, TriangleAlert, X } from "lucide-react";
import { Button } from "@/components/button";
import {
markAiProposalRunInTerminal,
rejectAiProposal,
type AiProposal,
} from "../ai-api";
import { ProposalCard } from "../ProposalCard";
interface TerminalProposalCardProps {
proposal: AiProposal;
hostId: number;
onRunInTerminal: (command: string) => void;
onResolved: (
id: number,
status: "applied" | "rejected",
resultSummary?: string,
) => void;
}
/**
* A run_command proposal in the terminal-docked assistant runs in the user's
* already-open session instead of a hidden pooled connection, so approving it
* here is a different action from the main AiPanel's apply button. Every
* other proposal kind still goes through the normal apply flow.
*/
export function TerminalProposalCard({
proposal,
hostId,
onRunInTerminal,
onResolved,
}: TerminalProposalCardProps) {
const { t } = useTranslation();
const [busy, setBusy] = useState<"run" | "reject" | null>(null);
if (
proposal.kind !== "propose_run_command" ||
proposal.status !== "pending"
) {
return <ProposalCard proposal={proposal} onResolved={onResolved} />;
}
let payload: Record<string, unknown> = {};
try {
payload = JSON.parse(proposal.payload);
} catch {
payload = {};
}
const command = typeof payload.command === "string" ? payload.command : "";
const explanation =
typeof payload.explanation === "string" ? payload.explanation : "";
async function handleRun() {
if (!command) return;
setBusy("run");
try {
onRunInTerminal(command);
const result = await markAiProposalRunInTerminal(
proposal.id,
hostId,
`$ ${command}`,
);
onResolved(proposal.id, "applied", result.summary);
} catch (error) {
toast.error(getErrorMessage(error, t("ai.proposalApplyFailed")));
} finally {
setBusy(null);
}
}
async function handleReject() {
setBusy("reject");
try {
await rejectAiProposal(proposal.id);
onResolved(proposal.id, "rejected");
} catch (error) {
toast.error(getErrorMessage(error, t("ai.proposalRejectFailed")));
} finally {
setBusy(null);
}
}
return (
<div className="rounded-none border border-border bg-muted p-2.5">
<div className="flex items-center gap-1.5">
<span className="min-w-0 flex-1 truncate text-xs font-medium">
{t("ai.runCommandTitle")}
</span>
</div>
<pre className="mt-2 max-h-40 overflow-auto whitespace-pre-wrap break-all border border-border bg-background px-2 py-1.5 font-mono text-[11px] leading-snug">
{command}
</pre>
{explanation && (
<p className="mt-1.5 text-[11px] leading-snug text-muted-foreground">
{explanation}
</p>
)}
<p className="mt-2 flex items-center gap-1 text-[10px] text-muted-foreground">
<TriangleAlert size={11} className="shrink-0" />
{t("ai.runsInOpenTerminal")}
</p>
<div className="mt-2 flex gap-1.5">
<Button
size="sm"
className="h-7 flex-1 text-xs border-accent-brand/40 text-accent-brand hover:bg-accent-brand/10 hover:text-accent-brand"
variant="outline"
disabled={busy !== null || !command}
onClick={handleRun}
>
{busy === "run" ? (
<Loader2 size={13} className="animate-spin" />
) : (
<Check size={13} />
)}
{t("ai.runInTerminal")}
</Button>
<Button
size="sm"
className="h-7 flex-1 text-xs"
variant="outline"
disabled={busy !== null}
onClick={handleReject}
>
{busy === "reject" ? (
<Loader2 size={13} className="animate-spin" />
) : (
<X size={13} />
)}
{t("ai.reject")}
</Button>
</div>
</div>
);
}
@@ -0,0 +1,68 @@
import { useEffect, useState } from "react";
export interface AiAvailability {
/** Admin kill switch. When false the feature does not exist for anyone. */
globallyEnabled: boolean;
/** Global switch and the user's own opt-in, which is what the rail needs. */
userEnabled: boolean;
/** False until the status call answers, so nothing flashes in or out. */
loaded: boolean;
}
/** Fired when the AI status may have changed, so every surface re-reads it. */
export const AI_STATUS_CHANGED_EVENT = "aiStatusChanged";
export function notifyAiStatusChanged(): void {
window.dispatchEvent(new Event(AI_STATUS_CHANGED_EVENT));
}
/**
* One source of truth for whether any AI surface should render.
*
* The admin global is a hard kill switch: when it is off the assistant is
* hidden everywhere for everyone, including the tab bar, command palette,
* mobile bar, and the navigation visibility toggles, so nobody can reach or
* even see an entry the server would refuse anyway.
*/
export function useAiAvailability(): AiAvailability {
const [state, setState] = useState<AiAvailability>({
globallyEnabled: false,
userEnabled: false,
loaded: false,
});
useEffect(() => {
let cancelled = false;
const refresh = () => {
import("./ai-api")
.then(({ getAiStatus }) => getAiStatus())
.then((status) => {
if (cancelled) return;
setState({
globallyEnabled: status.globallyEnabled,
userEnabled: status.globallyEnabled && status.enabled,
loaded: true,
});
})
.catch(() => {
if (!cancelled)
setState({
globallyEnabled: false,
userEnabled: false,
loaded: true,
});
});
};
refresh();
window.addEventListener(AI_STATUS_CHANGED_EVENT, refresh);
// The profile toggle and onboarding still fire this older event.
window.addEventListener("hiddenRailTabsChanged", refresh);
return () => {
cancelled = true;
window.removeEventListener(AI_STATUS_CHANGED_EVENT, refresh);
window.removeEventListener("hiddenRailTabsChanged", refresh);
};
}, []);
return state;
}
+220
View File
@@ -0,0 +1,220 @@
import { getErrorMessage } from "@/lib/error-message";
import { useCallback, useRef, useState } from "react";
import { authApi } from "@/main-axios";
import type { AiProposal } from "./ai-api";
/**
* Reuses whatever base authApi resolved to, so the stream follows the same
* dev proxy, Electron localhost and reverse-proxy base path rules as every
* other call instead of hardcoding an origin.
*/
function streamUrl(): string {
const base = (authApi.defaults.baseURL ?? "").replace(/\/+$/, "");
return `${base}/ai/chat/stream`;
}
/**
* Drives the chat stream.
*
* EventSource cannot POST, and the request carries a message body, so this
* reads the SSE frames off a fetch response by hand.
*/
export interface ToolActivity {
id: string;
name: string;
arguments: Record<string, unknown>;
result?: unknown;
}
export interface StreamState {
streaming: boolean;
assistantText: string;
tools: ToolActivity[];
proposals: AiProposal[];
error: string | null;
conversationId: number | null;
}
const INITIAL: StreamState = {
streaming: false,
assistantText: "",
tools: [],
proposals: [],
error: null,
conversationId: null,
};
export function useAiStream() {
const [state, setState] = useState<StreamState>(INITIAL);
const abortRef = useRef<AbortController | null>(null);
const reset = useCallback(() => {
setState(INITIAL);
}, []);
const stop = useCallback(() => {
abortRef.current?.abort();
abortRef.current = null;
setState((prev) => ({ ...prev, streaming: false }));
}, []);
const send = useCallback(
async (input: {
message: string;
providerId: number;
model?: string;
conversationId?: number | null;
activeTab?: string | null;
onComplete?: (conversationId: number | null, reply: string) => void;
}) => {
abortRef.current?.abort();
const controller = new AbortController();
abortRef.current = controller;
setState({
streaming: true,
assistantText: "",
tools: [],
proposals: [],
error: null,
conversationId: input.conversationId ?? null,
});
let conversationId = input.conversationId ?? null;
let replyText = "";
let toolSequence = 0;
try {
const response = await fetch(streamUrl(), {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
signal: controller.signal,
body: JSON.stringify({
message: input.message,
providerId: input.providerId,
model: input.model,
conversationId: input.conversationId ?? undefined,
activeTab: input.activeTab ?? undefined,
}),
});
if (!response.ok) {
let message = "The assistant could not be reached";
try {
message = (await response.json()).error ?? message;
} catch {
// Keep the generic message.
}
setState((prev) => ({ ...prev, streaming: false, error: message }));
return;
}
const reader = response.body?.getReader();
if (!reader) {
setState((prev) => ({
...prev,
streaming: false,
error: "The assistant returned an empty response",
}));
return;
}
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
let newlineIndex = buffer.indexOf("\n");
while (newlineIndex !== -1) {
const line = buffer.slice(0, newlineIndex).trim();
buffer = buffer.slice(newlineIndex + 1);
newlineIndex = buffer.indexOf("\n");
if (!line.startsWith("data:")) continue;
let event: any;
try {
event = JSON.parse(line.slice(5).trim());
} catch {
continue;
}
if (event.type === "conversation") {
conversationId = event.conversationId;
setState((prev) => ({
...prev,
conversationId: event.conversationId,
}));
} else if (event.type === "token") {
replyText += event.text;
setState((prev) => ({
...prev,
assistantText: prev.assistantText + event.text,
}));
} else if (event.type === "tool_call") {
// The id is assigned here rather than inside the updater: React
// may run an updater more than once, and deriving the id from
// prev.tools.length appended a duplicate entry that no result
// could ever match, leaving it stuck on "running".
const activityId = `tool-${toolSequence++}`;
setState((prev) => ({
...prev,
tools: [
...prev.tools,
{
id: activityId,
name: event.name,
arguments: event.arguments ?? {},
},
],
}));
} else if (event.type === "tool_result") {
setState((prev) => {
const tools = [...prev.tools];
// Attach to the most recent call of this tool awaiting a result.
for (let i = tools.length - 1; i >= 0; i -= 1) {
if (tools[i].name === event.name && !("result" in tools[i])) {
tools[i] = { ...tools[i], result: event.result };
break;
}
}
return { ...prev, tools };
});
} else if (event.type === "proposal") {
setState((prev) => ({
...prev,
proposals: [...prev.proposals, event.proposal],
}));
} else if (event.type === "error") {
setState((prev) => ({ ...prev, error: event.message }));
}
}
}
setState((prev) => ({ ...prev, streaming: false }));
input.onComplete?.(conversationId, replyText);
} catch (error) {
if (controller.signal.aborted) {
setState((prev) => ({ ...prev, streaming: false }));
return;
}
setState((prev) => ({
...prev,
streaming: false,
error: getErrorMessage(error, "The assistant stopped unexpectedly"),
}));
} finally {
abortRef.current = null;
}
},
[],
);
return { state, send, stop, reset, setState };
}
+120
View File
@@ -0,0 +1,120 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { getSSHHosts } from "@/api/ssh-host-management-api";
import { getSnippets } from "@/api/snippets-api";
import { invokeAction } from "@termix/plugin-sdk/frontend";
/**
* Backs the @-mention picker in the composer.
*
* A mention is only ever a name the assistant reads in the message text; it
* does not grant access to anything. The assistant still has to call a read
* tool to see the referenced item, so every access stays visible in the
* transcript rather than being silently attached to the prompt.
*/
export type MentionKind = "host" | "snippet" | "automation";
export interface MentionItem {
kind: MentionKind;
id: number;
label: string;
detail?: string;
}
/** Extracts the "@partial" the caret currently sits in, if any. */
export function activeMentionQuery(
value: string,
caret: number,
): { query: string; start: number } | null {
const before = value.slice(0, caret);
const at = before.lastIndexOf("@");
if (at === -1) return null;
// Only trigger at a word boundary, so an email address does not open it.
if (at > 0 && !/\s/.test(before[at - 1])) return null;
const query = before.slice(at + 1);
if (/\s/.test(query)) return null;
return { query, start: at };
}
/** The automations plugin's list, or nothing when it is not running. */
async function listAutomationsForMentions(): Promise<
{ id: number; name: string }[]
> {
const result = await invokeAction("automations.list");
return Array.isArray(result)
? (result as { id: number; name: string }[])
: [];
}
export function useMentions(enabled: boolean) {
const [items, setItems] = useState<MentionItem[]>([]);
useEffect(() => {
if (!enabled) return;
let cancelled = false;
(async () => {
const [hosts, snippets, automations] = await Promise.allSettled([
getSSHHosts(),
getSnippets(),
// Through the automations plugin's action, so the assistant works
// without it and never imports its code.
listAutomationsForMentions(),
]);
if (cancelled) return;
const collected: MentionItem[] = [];
if (hosts.status === "fulfilled") {
for (const host of (hosts.value ?? []) as any[]) {
collected.push({
kind: "host",
id: host.id,
label: host.name || host.ip,
detail: host.ip,
});
}
}
if (snippets.status === "fulfilled") {
for (const snippet of (snippets.value ?? []) as any[]) {
collected.push({
kind: "snippet",
id: snippet.id,
label: snippet.name,
});
}
}
if (automations.status === "fulfilled") {
for (const automation of (automations.value ?? []) as any[]) {
collected.push({
kind: "automation",
id: automation.id,
label: automation.name,
});
}
}
setItems(collected);
})();
return () => {
cancelled = true;
};
}, [enabled]);
const search = useCallback(
(query: string): MentionItem[] => {
const needle = query.trim().toLowerCase();
const matches = needle
? items.filter((item) => item.label.toLowerCase().includes(needle))
: items;
return matches.slice(0, 8);
},
[items],
);
return useMemo(() => ({ items, search }), [items, search]);
}
@@ -6,7 +6,7 @@ import {
screen,
waitFor,
} from "@testing-library/react";
import type { AiProvider } from "@/api/ai-api";
import type { AiProvider } from "../../src/frontend/ai-api";
const api = vi.hoisted(() => ({
createAiProvider: vi.fn(),
@@ -16,7 +16,7 @@ const api = vi.hoisted(() => ({
updateAiProvider: vi.fn(),
}));
vi.mock("@/api/ai-api", () => api);
vi.mock("../../src/frontend/ai-api", () => api);
vi.mock("react-i18next", () => ({
useTranslation: () => ({ t: (key: string) => key }),
}));
@@ -24,7 +24,7 @@ vi.mock("sonner", () => ({
toast: { success: vi.fn(), error: vi.fn() },
}));
import { AiProviderSettings } from "@/features/ai/AiProviderSettings";
import { AiProviderSettings } from "../../src/frontend/AiProviderSettings";
const provider: AiProvider = {
id: 7,
@@ -0,0 +1,53 @@
import { afterEach, describe, expect, it } from "vitest";
import {
renderWithApp,
type RenderedPluginApp,
} from "@termix/plugin-sdk/testing";
import type { PluginManifest } from "@termix/plugin-sdk/manifest";
import * as plugin from "../../src/frontend/index";
import manifestJson from "../../manifest.json";
import locales from "../../locales/en.json";
const manifest = manifestJson as unknown as PluginManifest;
const declared = (views?: Array<{ id: string }>) =>
(views ?? []).map((view) => view.id).sort();
let rendered: RenderedPluginApp | null = null;
afterEach(async () => {
await rendered?.deactivate();
rendered = null;
});
describe(`${manifest.id} activate`, () => {
it("activates and registers only views its manifest declares", async () => {
rendered = await renderWithApp(plugin, { manifest, locales });
const contributes = manifest.contributes as unknown as Record<
string,
Array<{ id: string }> | undefined
>;
const tabs = declared(contributes.tabs);
// A rail panel may reuse one of the plugin's tab ids.
const panels = [...declared(contributes.panels), ...tabs];
const cards = declared(contributes.dashboardCards);
for (const id of rendered.registered.tabs()) expect(tabs).toContain(id);
for (const id of rendered.registered.panels()) expect(panels).toContain(id);
for (const id of rendered.registered.dashboardCards()) {
expect(cards).toContain(id);
}
});
it("removes everything it registered on deactivate", async () => {
const app = await renderWithApp(plugin, { manifest, locales });
await app.deactivate();
expect(app.registered.tabs()).toEqual([]);
expect(app.registered.panels()).toEqual([]);
expect(app.registered.railItems()).toEqual([]);
expect(app.registered.hostActions()).toEqual([]);
expect(app.registered.hostEditorSections()).toEqual([]);
expect(app.registered.dashboardCards()).toEqual([]);
expect(app.registered.settingsComponents()).toEqual([]);
expect(app.registered.actions()).toEqual([]);
});
});
+1 -1
View File
@@ -4,7 +4,7 @@ import {
humanize,
mentionLabel,
toolLabel,
} from "@/features/ai/labels";
} from "../../src/frontend/labels";
import { AI_TOOLS } from "../../src/backend/tools/catalog.js";
describe("humanize", () => {
@@ -0,0 +1,109 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { renderHook, waitFor, act } from "@testing-library/react";
const getAiStatus = vi.fn();
vi.mock("../../src/frontend/ai-api", () => ({ getAiStatus }));
const { useAiAvailability, notifyAiStatusChanged } =
await import("../../src/frontend/use-ai-availability");
beforeEach(() => {
getAiStatus.mockReset();
});
afterEach(() => {
vi.restoreAllMocks();
});
describe("useAiAvailability", () => {
it("starts hidden and unloaded before the status answers", () => {
getAiStatus.mockReturnValue(new Promise(() => {}));
const { result } = renderHook(() => useAiAvailability());
expect(result.current).toEqual({
globallyEnabled: false,
userEnabled: false,
loaded: false,
});
});
it("reports both gates open when the admin and the user enabled it", async () => {
getAiStatus.mockResolvedValue({
globallyEnabled: true,
enabled: true,
allowReadOnlyCommands: false,
});
const { result } = renderHook(() => useAiAvailability());
await waitFor(() => expect(result.current.loaded).toBe(true));
expect(result.current.globallyEnabled).toBe(true);
expect(result.current.userEnabled).toBe(true);
});
it("keeps userEnabled false when the user has not opted in", async () => {
getAiStatus.mockResolvedValue({
globallyEnabled: true,
enabled: false,
allowReadOnlyCommands: false,
});
const { result } = renderHook(() => useAiAvailability());
await waitFor(() => expect(result.current.loaded).toBe(true));
expect(result.current.globallyEnabled).toBe(true);
expect(result.current.userEnabled).toBe(false);
});
it("the admin kill switch wins over the user opt-in", async () => {
getAiStatus.mockResolvedValue({
globallyEnabled: false,
enabled: true,
allowReadOnlyCommands: true,
});
const { result } = renderHook(() => useAiAvailability());
await waitFor(() => expect(result.current.loaded).toBe(true));
expect(result.current.globallyEnabled).toBe(false);
expect(result.current.userEnabled).toBe(false);
});
it("treats a failed status call as off", async () => {
getAiStatus.mockRejectedValue(new Error("boom"));
const { result } = renderHook(() => useAiAvailability());
await waitFor(() => expect(result.current.loaded).toBe(true));
expect(result.current.globallyEnabled).toBe(false);
expect(result.current.userEnabled).toBe(false);
});
it("re-reads the status when notified", async () => {
getAiStatus.mockResolvedValue({
globallyEnabled: true,
enabled: true,
allowReadOnlyCommands: false,
});
const { result } = renderHook(() => useAiAvailability());
await waitFor(() => expect(result.current.globallyEnabled).toBe(true));
getAiStatus.mockResolvedValue({
globallyEnabled: false,
enabled: true,
allowReadOnlyCommands: false,
});
act(() => notifyAiStatusChanged());
await waitFor(() => expect(result.current.globallyEnabled).toBe(false));
expect(result.current.userEnabled).toBe(false);
});
it("also re-reads on the older hiddenRailTabsChanged event", async () => {
getAiStatus.mockResolvedValue({
globallyEnabled: true,
enabled: false,
allowReadOnlyCommands: false,
});
const { result } = renderHook(() => useAiAvailability());
await waitFor(() => expect(result.current.loaded).toBe(true));
getAiStatus.mockResolvedValue({
globallyEnabled: true,
enabled: true,
allowReadOnlyCommands: false,
});
act(() => window.dispatchEvent(new Event("hiddenRailTabsChanged")));
await waitFor(() => expect(result.current.userEnabled).toBe(true));
});
});