Merge pull request #5043 from nesquena/release/stage-4964-postinstall
Release: extension post-install next steps in gallery (#4964, fixes #4959)
This commit is contained in:
commit
2d9422adf2
6 changed files with 309 additions and 7 deletions
|
|
@ -5,6 +5,8 @@
|
|||
|
||||
### Fixed
|
||||
|
||||
- **Installing an extension now shows its post-install next steps.** Some extensions need a follow-up step after install (run a setup command, restart, configure a key). The Settings → Extensions gallery now renders that guidance from the extension's own registry metadata — generic `post_install` text and lifecycle requirements, with an optional docs link — right on the card, plus a "see the card for next steps" install toast. The guidance is registry-driven (no vendor special-casing), every value is HTML-escaped, any docs URL is validated to safe http(s) before it's linked, and the note degrades cleanly when an extension declares no post-install steps. Thanks @franksong2702. (#4964, fixes #4959)
|
||||
|
||||
- **The Settings → Extensions diagnostics panel now shows per-extension sidecar runtime status.** When an extension's loopback sidecar health response includes an optional top-level `runtime` object, the diagnostics panel parses it and renders only allowlisted scalar fields (sidecar/native-host/bridge status, last-seen time, and the loopback origin) so you can see at a glance whether an extension's helper process is running, waiting, or stale — without WebUI depending on any one extension's private payload shape. All values are status-enum-allowlisted, HTML-escaped, timestamp-validated, and origin-limited to `http://127.0.0.1`/`localhost`; a missing or malformed `runtime` object degrades cleanly, and a failed diagnostics refresh now renders the error instead of staying stuck on "Loading…". Thanks @franksong2702. (#4979)
|
||||
|
||||
- **The "Response complete" notification now previews the final answer, not the start of the stream.** The browser/PWA completion notification body was built from the live streaming accumulator sliced to 100 chars, so it showed the *beginning* of the response (often a preamble or empty) rather than what the user actually got. It now derives the preview from the settled final answer (anchor projection → message content → raw text), strips inline thinking, normalizes whitespace, and truncates at 100 chars — and works for completions that finish while the session isn't the active pane. Thanks @claw-io. (#5036, fixes #5035)
|
||||
|
|
|
|||
|
|
@ -58,6 +58,13 @@ applies — installed extension code runs with full session authority — so onl
|
|||
install extensions from the vetted gallery or sources you trust as much as the
|
||||
WebUI source itself.
|
||||
|
||||
Some gallery entries need more than WebUI assets. If an extension declares
|
||||
post-install guidance or lifecycle requirements such as a loopback sidecar or a
|
||||
native host, Settings -> Extensions shows a **Next step** note on the card after
|
||||
install. For example, Desktop Companion can install the WebUI bridge from the
|
||||
gallery, but the desktop pet is only visible after the local Desktop Companion
|
||||
app is started.
|
||||
|
||||
### Manual / advanced configuration (optional)
|
||||
|
||||
`HERMES_WEBUI_EXTENSION_DIR` is **optional** and overrides the managed default.
|
||||
|
|
|
|||
140
static/i18n.js
140
static/i18n.js
|
|
@ -680,6 +680,16 @@ const LOCALES = {
|
|||
ext_gallery_version: 'Version',
|
||||
ext_gallery_author: 'Author',
|
||||
ext_gallery_capabilities: 'Capabilities',
|
||||
ext_gallery_next_step: 'Next step',
|
||||
ext_gallery_after_install: 'After install',
|
||||
ext_gallery_local_component_required: 'This extension has a local component. Start the required local app or sidecar after installing.',
|
||||
ext_gallery_local_app_label: 'Local app',
|
||||
ext_gallery_required_suffix: '{0} required',
|
||||
ext_gallery_sidecar_required: 'Sidecar required',
|
||||
ext_gallery_native_host_required: 'Native host required',
|
||||
ext_gallery_open_setup_guide: 'Open setup guide',
|
||||
ext_gallery_install_restart_required: 'Extension installed. Restart required to apply.',
|
||||
ext_gallery_install_followup: 'Extension installed. See the card for next steps.',
|
||||
ext_gallery_install_ok: 'Extension installed successfully.',
|
||||
ext_gallery_uninstall_ok: 'Extension uninstalled.',
|
||||
settings_plugins_title: 'Plugins',
|
||||
|
|
@ -2301,6 +2311,16 @@ const LOCALES = {
|
|||
ext_gallery_version: 'Version',
|
||||
ext_gallery_author: 'Author',
|
||||
ext_gallery_capabilities: 'Capabilities',
|
||||
ext_gallery_next_step: 'Prossimo passo',
|
||||
ext_gallery_after_install: "Dopo l'installazione",
|
||||
ext_gallery_local_component_required: "Questa estensione ha un componente locale. Avvia l'app locale o il sidecar richiesto dopo l'installazione.",
|
||||
ext_gallery_local_app_label: 'App locale',
|
||||
ext_gallery_required_suffix: '{0} richiesto',
|
||||
ext_gallery_sidecar_required: 'Sidecar richiesto',
|
||||
ext_gallery_native_host_required: 'Host nativo richiesto',
|
||||
ext_gallery_open_setup_guide: 'Apri guida di configurazione',
|
||||
ext_gallery_install_restart_required: 'Estensione installata. Riavvio richiesto per applicare le modifiche.',
|
||||
ext_gallery_install_followup: 'Estensione installata. Consulta la scheda per i prossimi passi.',
|
||||
ext_gallery_install_ok: 'Extension installed successfully.',
|
||||
ext_gallery_uninstall_ok: 'Extension uninstalled.',
|
||||
settings_plugins_title: 'Plugins', // TODO: translate
|
||||
|
|
@ -3913,6 +3933,16 @@ const LOCALES = {
|
|||
ext_gallery_version: 'Version',
|
||||
ext_gallery_author: 'Author',
|
||||
ext_gallery_capabilities: 'Capabilities',
|
||||
ext_gallery_next_step: '次の手順',
|
||||
ext_gallery_after_install: 'インストール後',
|
||||
ext_gallery_local_component_required: 'この拡張機能にはローカルコンポーネントが必要です。インストール後に必要なローカルアプリまたはサイドカーを起動してください。',
|
||||
ext_gallery_local_app_label: 'ローカルアプリ',
|
||||
ext_gallery_required_suffix: '{0} が必要',
|
||||
ext_gallery_sidecar_required: 'サイドカーが必要',
|
||||
ext_gallery_native_host_required: 'ネイティブホストが必要',
|
||||
ext_gallery_open_setup_guide: 'セットアップガイドを開く',
|
||||
ext_gallery_install_restart_required: '拡張機能をインストールしました。適用するには再起動が必要です。',
|
||||
ext_gallery_install_followup: '拡張機能をインストールしました。次の手順はカードで確認してください。',
|
||||
ext_gallery_install_ok: 'Extension installed successfully.',
|
||||
ext_gallery_uninstall_ok: 'Extension uninstalled.',
|
||||
settings_plugins_title: 'プラグイン',
|
||||
|
|
@ -6233,6 +6263,16 @@ const LOCALES = {
|
|||
ext_gallery_version: 'Version',
|
||||
ext_gallery_author: 'Author',
|
||||
ext_gallery_capabilities: 'Capabilities',
|
||||
ext_gallery_next_step: 'Следующий шаг',
|
||||
ext_gallery_after_install: 'После установки',
|
||||
ext_gallery_local_component_required: 'У этого расширения есть локальный компонент. После установки запустите требуемое локальное приложение или sidecar.',
|
||||
ext_gallery_local_app_label: 'Локальное приложение',
|
||||
ext_gallery_required_suffix: 'Требуется {0}',
|
||||
ext_gallery_sidecar_required: 'Требуется sidecar',
|
||||
ext_gallery_native_host_required: 'Требуется нативный хост',
|
||||
ext_gallery_open_setup_guide: 'Открыть руководство по настройке',
|
||||
ext_gallery_install_restart_required: 'Расширение установлено. Для применения требуется перезапуск.',
|
||||
ext_gallery_install_followup: 'Расширение установлено. Следующие шаги смотрите на карточке.',
|
||||
ext_gallery_install_ok: 'Extension installed successfully.',
|
||||
ext_gallery_uninstall_ok: 'Extension uninstalled.',
|
||||
settings_plugins_title: 'Plugins', // TODO: translate
|
||||
|
|
@ -7770,6 +7810,16 @@ const LOCALES = {
|
|||
ext_gallery_version: 'Version',
|
||||
ext_gallery_author: 'Author',
|
||||
ext_gallery_capabilities: 'Capabilities',
|
||||
ext_gallery_next_step: 'Siguiente paso',
|
||||
ext_gallery_after_install: 'Después de instalar',
|
||||
ext_gallery_local_component_required: 'Esta extensión tiene un componente local. Después de instalarla, inicia la app local o el sidecar requerido.',
|
||||
ext_gallery_local_app_label: 'App local',
|
||||
ext_gallery_required_suffix: 'Se requiere {0}',
|
||||
ext_gallery_sidecar_required: 'Se requiere sidecar',
|
||||
ext_gallery_native_host_required: 'Se requiere host nativo',
|
||||
ext_gallery_open_setup_guide: 'Abrir guía de configuración',
|
||||
ext_gallery_install_restart_required: 'Extensión instalada. Reinicio requerido para aplicar.',
|
||||
ext_gallery_install_followup: 'Extensión instalada. Consulta la tarjeta para ver los próximos pasos.',
|
||||
ext_gallery_install_ok: 'Extension installed successfully.',
|
||||
ext_gallery_uninstall_ok: 'Extension uninstalled.',
|
||||
settings_plugins_title: 'Plugins', // TODO: translate
|
||||
|
|
@ -8991,6 +9041,16 @@ const LOCALES = {
|
|||
ext_gallery_version: 'Version',
|
||||
ext_gallery_author: 'Author',
|
||||
ext_gallery_capabilities: 'Capabilities',
|
||||
ext_gallery_next_step: 'Nächster Schritt',
|
||||
ext_gallery_after_install: 'Nach der Installation',
|
||||
ext_gallery_local_component_required: 'Diese Erweiterung hat eine lokale Komponente. Starte nach der Installation die erforderliche lokale App oder den Sidecar.',
|
||||
ext_gallery_local_app_label: 'Lokale App',
|
||||
ext_gallery_required_suffix: '{0} erforderlich',
|
||||
ext_gallery_sidecar_required: 'Sidecar erforderlich',
|
||||
ext_gallery_native_host_required: 'Nativer Host erforderlich',
|
||||
ext_gallery_open_setup_guide: 'Einrichtungsanleitung öffnen',
|
||||
ext_gallery_install_restart_required: 'Erweiterung installiert. Neustart zum Anwenden erforderlich.',
|
||||
ext_gallery_install_followup: 'Erweiterung installiert. Die nächsten Schritte stehen auf der Karte.',
|
||||
ext_gallery_install_ok: 'Extension installed successfully.',
|
||||
ext_gallery_uninstall_ok: 'Extension uninstalled.',
|
||||
settings_plugins_title: 'Plugins', // TODO: translate
|
||||
|
|
@ -10879,6 +10939,16 @@ const LOCALES = {
|
|||
ext_gallery_version: 'Version',
|
||||
ext_gallery_author: 'Author',
|
||||
ext_gallery_capabilities: 'Capabilities',
|
||||
ext_gallery_next_step: '下一步',
|
||||
ext_gallery_after_install: '安装后',
|
||||
ext_gallery_local_component_required: '此扩展包含本地组件。安装后请启动所需的本地应用或 sidecar。',
|
||||
ext_gallery_local_app_label: '本地应用',
|
||||
ext_gallery_required_suffix: '需要 {0}',
|
||||
ext_gallery_sidecar_required: '需要 sidecar',
|
||||
ext_gallery_native_host_required: '需要原生主机',
|
||||
ext_gallery_open_setup_guide: '打开设置指南',
|
||||
ext_gallery_install_restart_required: '扩展已安装。需要重启后生效。',
|
||||
ext_gallery_install_followup: '扩展已安装。请查看卡片中的后续步骤。',
|
||||
ext_gallery_install_ok: 'Extension installed successfully.',
|
||||
ext_gallery_uninstall_ok: 'Extension uninstalled.',
|
||||
settings_tab_system: '系统',
|
||||
|
|
@ -11722,6 +11792,16 @@ const LOCALES = {
|
|||
ext_gallery_version: 'Version',
|
||||
ext_gallery_author: 'Author',
|
||||
ext_gallery_capabilities: 'Capabilities',
|
||||
ext_gallery_next_step: '下一步',
|
||||
ext_gallery_after_install: '安裝後',
|
||||
ext_gallery_local_component_required: '此擴充功能包含本機元件。安裝後請啟動所需的本機應用程式或 sidecar。',
|
||||
ext_gallery_local_app_label: '本機應用程式',
|
||||
ext_gallery_required_suffix: '需要 {0}',
|
||||
ext_gallery_sidecar_required: '需要 sidecar',
|
||||
ext_gallery_native_host_required: '需要原生主機',
|
||||
ext_gallery_open_setup_guide: '開啟設定指南',
|
||||
ext_gallery_install_restart_required: '擴充功能已安裝。需要重新啟動後套用。',
|
||||
ext_gallery_install_followup: '擴充功能已安裝。請查看卡片中的後續步驟。',
|
||||
ext_gallery_install_ok: 'Extension installed successfully.',
|
||||
ext_gallery_uninstall_ok: 'Extension uninstalled.',
|
||||
settings_plugins_title: '外掛',
|
||||
|
|
@ -13230,6 +13310,16 @@ const LOCALES = {
|
|||
ext_gallery_version: 'Version',
|
||||
ext_gallery_author: 'Author',
|
||||
ext_gallery_capabilities: 'Capabilities',
|
||||
ext_gallery_next_step: 'Próximo passo',
|
||||
ext_gallery_after_install: 'Após instalar',
|
||||
ext_gallery_local_component_required: 'Esta extensão tem um componente local. Depois de instalar, inicie o app local ou sidecar necessário.',
|
||||
ext_gallery_local_app_label: 'App local',
|
||||
ext_gallery_required_suffix: '{0} necessário',
|
||||
ext_gallery_sidecar_required: 'Sidecar necessário',
|
||||
ext_gallery_native_host_required: 'Host nativo necessário',
|
||||
ext_gallery_open_setup_guide: 'Abrir guia de configuração',
|
||||
ext_gallery_install_restart_required: 'Extensão instalada. Reinício necessário para aplicar.',
|
||||
ext_gallery_install_followup: 'Extensão instalada. Veja o cartão para os próximos passos.',
|
||||
ext_gallery_install_ok: 'Extension installed successfully.',
|
||||
ext_gallery_uninstall_ok: 'Extension uninstalled.',
|
||||
settings_plugins_title: 'Plugins', // TODO: translate
|
||||
|
|
@ -14744,6 +14834,16 @@ const LOCALES = {
|
|||
ext_gallery_version: 'Version',
|
||||
ext_gallery_author: 'Author',
|
||||
ext_gallery_capabilities: 'Capabilities',
|
||||
ext_gallery_next_step: '다음 단계',
|
||||
ext_gallery_after_install: '설치 후',
|
||||
ext_gallery_local_component_required: '이 확장에는 로컬 구성 요소가 있습니다. 설치 후 필요한 로컬 앱이나 사이드카를 시작하세요.',
|
||||
ext_gallery_local_app_label: '로컬 앱',
|
||||
ext_gallery_required_suffix: '{0} 필요',
|
||||
ext_gallery_sidecar_required: '사이드카 필요',
|
||||
ext_gallery_native_host_required: '네이티브 호스트 필요',
|
||||
ext_gallery_open_setup_guide: '설정 가이드 열기',
|
||||
ext_gallery_install_restart_required: '확장이 설치되었습니다. 적용하려면 다시 시작해야 합니다.',
|
||||
ext_gallery_install_followup: '확장이 설치되었습니다. 카드에서 다음 단계를 확인하세요.',
|
||||
ext_gallery_install_ok: 'Extension installed successfully.',
|
||||
ext_gallery_uninstall_ok: 'Extension uninstalled.',
|
||||
settings_plugins_title: 'Plugins', // TODO: translate
|
||||
|
|
@ -16368,6 +16468,16 @@ const LOCALES = {
|
|||
ext_gallery_version: 'Version',
|
||||
ext_gallery_author: 'Author',
|
||||
ext_gallery_capabilities: 'Capabilities',
|
||||
ext_gallery_next_step: 'Étape suivante',
|
||||
ext_gallery_after_install: 'Après l’installation',
|
||||
ext_gallery_local_component_required: 'Cette extension comporte un composant local. Après l’installation, démarrez l’application locale ou le sidecar requis.',
|
||||
ext_gallery_local_app_label: 'Application locale',
|
||||
ext_gallery_required_suffix: '{0} requis',
|
||||
ext_gallery_sidecar_required: 'Sidecar requis',
|
||||
ext_gallery_native_host_required: 'Hôte natif requis',
|
||||
ext_gallery_open_setup_guide: 'Ouvrir le guide de configuration',
|
||||
ext_gallery_install_restart_required: 'Extension installée. Redémarrage requis pour appliquer.',
|
||||
ext_gallery_install_followup: 'Extension installée. Consultez la carte pour les prochaines étapes.',
|
||||
ext_gallery_install_ok: 'Extension installed successfully.',
|
||||
ext_gallery_uninstall_ok: 'Extension uninstalled.',
|
||||
settings_plugins_title: 'Plugins',
|
||||
|
|
@ -17948,6 +18058,16 @@ const LOCALES = {
|
|||
ext_gallery_version: 'Version',
|
||||
ext_gallery_author: 'Author',
|
||||
ext_gallery_capabilities: 'Capabilities',
|
||||
ext_gallery_next_step: 'Sonraki adım',
|
||||
ext_gallery_after_install: 'Kurulumdan sonra',
|
||||
ext_gallery_local_component_required: "Bu uzantının yerel bir bileşeni var. Kurulumdan sonra gerekli yerel uygulamayı veya sidecar'ı başlatın.",
|
||||
ext_gallery_local_app_label: 'Yerel uygulama',
|
||||
ext_gallery_required_suffix: '{0} gerekli',
|
||||
ext_gallery_sidecar_required: 'Sidecar gerekli',
|
||||
ext_gallery_native_host_required: 'Native host gerekli',
|
||||
ext_gallery_open_setup_guide: 'Kurulum kılavuzunu aç',
|
||||
ext_gallery_install_restart_required: 'Uzantı kuruldu. Uygulamak için yeniden başlatma gerekli.',
|
||||
ext_gallery_install_followup: 'Uzantı kuruldu. Sonraki adımlar için kartı kontrol edin.',
|
||||
ext_gallery_install_ok: 'Extension installed successfully.',
|
||||
ext_gallery_uninstall_ok: 'Extension uninstalled.',
|
||||
settings_plugins_title: 'Eklentiler',
|
||||
|
|
@ -19574,6 +19694,16 @@ const LOCALES = {
|
|||
ext_gallery_version: 'Version',
|
||||
ext_gallery_author: 'Author',
|
||||
ext_gallery_capabilities: 'Capabilities',
|
||||
ext_gallery_next_step: 'Następny krok',
|
||||
ext_gallery_after_install: 'Po instalacji',
|
||||
ext_gallery_local_component_required: 'To rozszerzenie ma komponent lokalny. Po instalacji uruchom wymaganą aplikację lokalną lub sidecar.',
|
||||
ext_gallery_local_app_label: 'Aplikacja lokalna',
|
||||
ext_gallery_required_suffix: 'Wymagane: {0}',
|
||||
ext_gallery_sidecar_required: 'Wymagany sidecar',
|
||||
ext_gallery_native_host_required: 'Wymagany host natywny',
|
||||
ext_gallery_open_setup_guide: 'Otwórz przewodnik konfiguracji',
|
||||
ext_gallery_install_restart_required: 'Rozszerzenie zainstalowane. Do zastosowania wymagany jest restart.',
|
||||
ext_gallery_install_followup: 'Rozszerzenie zainstalowane. Sprawdź kartę, aby zobaczyć kolejne kroki.',
|
||||
ext_gallery_install_ok: 'Extension installed successfully.',
|
||||
ext_gallery_uninstall_ok: 'Extension uninstalled.',
|
||||
settings_plugins_title: 'Wtyczki',
|
||||
|
|
@ -21088,6 +21218,16 @@ const LOCALES = {
|
|||
ext_gallery_version: 'Version',
|
||||
ext_gallery_author: 'Author',
|
||||
ext_gallery_capabilities: 'Capabilities',
|
||||
ext_gallery_next_step: 'Bước tiếp theo',
|
||||
ext_gallery_after_install: 'Sau khi cài đặt',
|
||||
ext_gallery_local_component_required: 'Tiện ích mở rộng này có thành phần cục bộ. Sau khi cài đặt, hãy khởi động ứng dụng cục bộ hoặc sidecar bắt buộc.',
|
||||
ext_gallery_local_app_label: 'Ứng dụng cục bộ',
|
||||
ext_gallery_required_suffix: 'Cần {0}',
|
||||
ext_gallery_sidecar_required: 'Cần sidecar',
|
||||
ext_gallery_native_host_required: 'Cần native host',
|
||||
ext_gallery_open_setup_guide: 'Mở hướng dẫn thiết lập',
|
||||
ext_gallery_install_restart_required: 'Đã cài đặt tiện ích mở rộng. Cần khởi động lại để áp dụng.',
|
||||
ext_gallery_install_followup: 'Đã cài đặt tiện ích mở rộng. Xem thẻ để biết các bước tiếp theo.',
|
||||
ext_gallery_install_ok: 'Extension installed successfully.',
|
||||
ext_gallery_uninstall_ok: 'Extension uninstalled.',
|
||||
settings_plugins_title: 'Plugin',
|
||||
|
|
|
|||
|
|
@ -8421,6 +8421,50 @@ function switchExtensionsTab(tab){
|
|||
if(tab==='gallery'&&!_extensionsGalleryLoaded) loadExtensionsGallery();
|
||||
}
|
||||
|
||||
function _extensionSafeHttpUrl(value){
|
||||
if(!value) return '';
|
||||
const raw=String(value).trim();
|
||||
if(!/^https?:\/\//i.test(raw)) return '';
|
||||
try{
|
||||
const url=new URL(raw);
|
||||
if(url.username||url.password) return '';
|
||||
return (url.protocol==='http:'||url.protocol==='https:')?url.href:'';
|
||||
}catch(_){
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function _extensionPostInstallNote(entry,isInstalled){
|
||||
const lifecycle=(entry&&entry.lifecycle&&typeof entry.lifecycle==='object')?entry.lifecycle:{};
|
||||
const post=(entry&&entry.post_install&&typeof entry.post_install==='object')?entry.post_install:null;
|
||||
const needsSidecar=!!lifecycle.sidecar_start_required;
|
||||
const needsNative=!!lifecycle.native_host_start_required;
|
||||
const summary=post&&post.summary?String(post.summary):(
|
||||
(needsSidecar||needsNative)
|
||||
? t('ext_gallery_local_component_required')
|
||||
: ''
|
||||
);
|
||||
if(!summary) return '';
|
||||
const docsUrl=_extensionSafeHttpUrl(post&&post.docs_url);
|
||||
const localAppLabel=post&&post.local_app_label?String(post.local_app_label):t('ext_gallery_local_app_label');
|
||||
const chips=[];
|
||||
if(post&&post.requires_local_app===true) chips.push(t('ext_gallery_required_suffix',localAppLabel));
|
||||
if(needsSidecar) chips.push(t('ext_gallery_sidecar_required'));
|
||||
if(needsNative) chips.push(t('ext_gallery_native_host_required'));
|
||||
const chipHtml=chips.length
|
||||
? '<div class="extension-gallery-next-chips">'+chips.map(item=>`<span>${esc(item)}</span>`).join('')+'</div>'
|
||||
: '';
|
||||
const docsHtml=docsUrl
|
||||
? `<a class="extension-gallery-next-link" href="${esc(docsUrl)}" target="_blank" rel="noopener noreferrer">${esc(t('ext_gallery_open_setup_guide'))}</a>`
|
||||
: '';
|
||||
return `<div class="extension-gallery-next-step">
|
||||
<div class="extension-gallery-next-label">${esc(t(isInstalled?'ext_gallery_next_step':'ext_gallery_after_install'))}</div>
|
||||
<div class="extension-gallery-next-summary">${esc(summary)}</div>
|
||||
${chipHtml}
|
||||
${docsHtml}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
async function loadExtensionsGallery(){
|
||||
_extensionsGalleryLoaded=true;
|
||||
const galleryEl=$('extensionsGallery');
|
||||
|
|
@ -8468,9 +8512,10 @@ function _renderExtensionsGallery(entries,statusData){
|
|||
const caps=Array.isArray(entry.capabilities)?entry.capabilities:[];
|
||||
const perms=entry.permissions||null;
|
||||
const isInstalled=installedIds.has(String(entry.id||''));
|
||||
const restartRequired=!!(entry.lifecycle&&entry.lifecycle.restart_required);
|
||||
const restartRequired=!!(entry.lifecycle&&(entry.lifecycle.restart_required||entry.lifecycle.webui_restart_required));
|
||||
const badgesHtml=caps.map(c=>`<span class="extension-gallery-badge">${esc(String(c))}</span>`).join('');
|
||||
const permsHtml=perms?`<details class="extension-gallery-perms"><summary data-i18n="ext_gallery_permissions_show">Permissions</summary><pre>${esc(JSON.stringify(perms,null,2))}</pre></details>`:'';
|
||||
const postInstallHtml=_extensionPostInstallNote(entry,isInstalled);
|
||||
const actionBtn=isInstalled
|
||||
?`<button class="extension-gallery-uninstall-btn" data-ext-uninstall-id="${id}" type="button" data-i18n="ext_gallery_uninstall">Uninstall</button>`
|
||||
:`<button class="extension-gallery-install-btn" data-ext-install-id="${id}" type="button" data-i18n="ext_gallery_install">Install</button>`;
|
||||
|
|
@ -8484,6 +8529,7 @@ function _renderExtensionsGallery(entries,statusData){
|
|||
</div>
|
||||
<div class="extension-gallery-desc">${desc}</div>
|
||||
${badgesHtml?'<div class="extension-gallery-badge-row">'+badgesHtml+'</div>':''}
|
||||
${postInstallHtml}
|
||||
${permsHtml}
|
||||
<div class="extension-gallery-actions">${actionBtn}</div>
|
||||
</div>`;
|
||||
|
|
@ -8511,15 +8557,18 @@ async function handleExtensionInstall(btn,entry){
|
|||
if(!btn||btn.disabled) return;
|
||||
const previousText=btn.textContent;
|
||||
btn.disabled=true;
|
||||
btn.textContent='Installing…';
|
||||
btn.textContent=t('ext_gallery_installing');
|
||||
try{
|
||||
const result=await api('/api/extensions/install',{method:'POST',body:JSON.stringify({
|
||||
id:entry.id,
|
||||
download_url:entry.download_url||entry.download,
|
||||
sha256:entry.sha256,
|
||||
})});
|
||||
const restart=!!(entry.lifecycle&&entry.lifecycle.restart_required);
|
||||
showToast(restart?'Extension installed. Restart required to apply.':'Extension installed successfully.');
|
||||
const restart=!!(entry.lifecycle&&(entry.lifecycle.restart_required||entry.lifecycle.webui_restart_required));
|
||||
const hasPostInstall=!!(entry.post_install||(entry.lifecycle&&(entry.lifecycle.sidecar_start_required||entry.lifecycle.native_host_start_required)));
|
||||
showToast(restart
|
||||
? t('ext_gallery_install_restart_required')
|
||||
: (hasPostInstall?t('ext_gallery_install_followup'):t('ext_gallery_install_ok')));
|
||||
_extensionsGalleryLoaded=false;
|
||||
await loadExtensionsGallery();
|
||||
}catch(e){
|
||||
|
|
|
|||
|
|
@ -4758,6 +4758,13 @@ main.main > #mainPlugin{display:none;}
|
|||
.extension-gallery-desc{font-size:12px;color:var(--muted);margin-top:6px;line-height:1.5;}
|
||||
.extension-gallery-badge-row{display:flex;flex-wrap:wrap;gap:4px;margin-top:8px;}
|
||||
.extension-gallery-badge{display:inline-flex;align-items:center;background:var(--code-bg);color:var(--muted);border-radius:999px;padding:2px 8px;font-size:10.5px;font-weight:600;font-family:var(--font-mono);}
|
||||
.extension-gallery-next-step{margin-top:10px;padding:9px 10px;border:1px solid var(--border);border-radius:8px;background:var(--subtle-bg,rgba(127,127,127,.06));}
|
||||
.extension-gallery-next-label{font-size:11px;font-weight:700;color:var(--text);text-transform:uppercase;letter-spacing:0;}
|
||||
.extension-gallery-next-summary{font-size:12px;color:var(--muted);line-height:1.45;margin-top:4px;}
|
||||
.extension-gallery-next-chips{display:flex;flex-wrap:wrap;gap:4px;margin-top:7px;}
|
||||
.extension-gallery-next-chips span{display:inline-flex;align-items:center;border:1px solid var(--border);border-radius:999px;color:var(--muted);font-size:10.5px;font-weight:600;padding:2px 7px;background:var(--card-bg,var(--sidebar-bg));}
|
||||
.extension-gallery-next-link{display:inline-flex;margin-top:7px;font-size:12px;font-weight:600;color:var(--accent);text-decoration:none;}
|
||||
.extension-gallery-next-link:hover{text-decoration:underline;}
|
||||
.extension-gallery-perms{margin-top:8px;font-size:12px;}
|
||||
.extension-gallery-perms summary{cursor:pointer;color:var(--muted);user-select:none;}
|
||||
.extension-gallery-perms pre{margin:6px 0 0;padding:8px;background:var(--code-bg);border-radius:4px;font-size:11px;overflow-x:auto;white-space:pre-wrap;word-break:break-all;}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,12 @@ def _locale_blocks() -> dict[str, str]:
|
|||
return blocks
|
||||
|
||||
|
||||
def _locale_string(block: str, key: str) -> str:
|
||||
match = re.search(rf"\b{re.escape(key)}:\s*([\"'])(.*?)\1", block, re.DOTALL)
|
||||
assert match, f"{key} not found in locale block"
|
||||
return match.group(2)
|
||||
|
||||
|
||||
def _contains_post_method(block: str) -> bool:
|
||||
"""Return True when a JS block contains a method: 'POST' style mutation."""
|
||||
return bool(re.search(r"\bmethod\s*:\s*([\"'`])POST\1", block))
|
||||
|
|
@ -207,6 +213,40 @@ def test_extensions_panel_toggle_uses_dedicated_endpoint_without_settings_or_ins
|
|||
assert "marketplace" not in combined.lower()
|
||||
|
||||
|
||||
def test_extensions_gallery_renders_post_install_guidance():
|
||||
url_block = _between("function _extensionSafeHttpUrl", "function _extensionPostInstallNote")
|
||||
gallery_block = _between("function _extensionPostInstallNote", "async function loadExtensionsGallery")
|
||||
render_block = _between("function _renderExtensionsGallery", "function _bindExtensionGalleryButtons")
|
||||
install_block = _between("async function handleExtensionInstall", "async function handleExtensionUninstall")
|
||||
|
||||
assert "/^https?:\\/\\//i.test(raw)" in url_block
|
||||
assert "url.username||url.password" in url_block
|
||||
assert "entry.post_install" in gallery_block
|
||||
assert "post&&post.docs_url" in gallery_block
|
||||
assert "sidecar_start_required" in gallery_block
|
||||
assert "native_host_start_required" in gallery_block
|
||||
assert "requires_local_app" in gallery_block
|
||||
assert "local_app_label" in gallery_block
|
||||
assert "t('ext_gallery_local_component_required')" in gallery_block
|
||||
assert "t('ext_gallery_local_app_label')" in gallery_block
|
||||
assert "t('ext_gallery_required_suffix',localAppLabel)" in gallery_block
|
||||
assert "t('ext_gallery_sidecar_required')" in gallery_block
|
||||
assert "t('ext_gallery_native_host_required')" in gallery_block
|
||||
assert "t('ext_gallery_open_setup_guide')" in gallery_block
|
||||
assert "t(isInstalled?'ext_gallery_next_step':'ext_gallery_after_install')" in gallery_block
|
||||
assert "target=\"_blank\"" in gallery_block
|
||||
assert "rel=\"noopener noreferrer\"" in gallery_block
|
||||
assert "extension-gallery-next-step" in gallery_block
|
||||
assert "esc(summary)" in gallery_block
|
||||
assert "esc(docsUrl)" in gallery_block
|
||||
assert "esc(item)" in gallery_block
|
||||
assert "_extensionPostInstallNote(entry,isInstalled)" in render_block
|
||||
assert "t('ext_gallery_install_restart_required')" in install_block
|
||||
assert "t('ext_gallery_install_followup')" in install_block
|
||||
assert "t('ext_gallery_install_ok')" in install_block
|
||||
assert "webui_restart_required" in install_block
|
||||
|
||||
|
||||
def test_copy_extensions_diagnostics_copies_current_sanitized_payload():
|
||||
copy_block = _between("function copyExtensionsDiagnostics", "// ── Plugins panel")
|
||||
|
||||
|
|
@ -228,14 +268,71 @@ def test_extensions_styles_are_scoped_to_extensions_panel():
|
|||
assert ".extension-sidecar-list" in STYLE_CSS
|
||||
assert ".extension-sidecar-runtime" in STYLE_CSS
|
||||
assert ".extension-sidecar-status-badge" in STYLE_CSS
|
||||
assert ".extension-gallery-next-step" in STYLE_CSS
|
||||
assert ".extension-gallery-next-link" in STYLE_CSS
|
||||
|
||||
|
||||
def test_extensions_tab_i18n_key_exists_for_all_locales():
|
||||
def test_extensions_i18n_keys_exist_for_all_locales():
|
||||
blocks = _locale_blocks()
|
||||
|
||||
assert len(blocks) == _locale_count()
|
||||
missing = [name for name, block in blocks.items() if "settings_tab_extensions" not in block]
|
||||
assert not missing, f"Locale(s) missing settings_tab_extensions: {missing}"
|
||||
required_keys = [
|
||||
"settings_tab_extensions",
|
||||
"ext_gallery_next_step",
|
||||
"ext_gallery_after_install",
|
||||
"ext_gallery_local_component_required",
|
||||
"ext_gallery_local_app_label",
|
||||
"ext_gallery_required_suffix",
|
||||
"ext_gallery_sidecar_required",
|
||||
"ext_gallery_native_host_required",
|
||||
"ext_gallery_open_setup_guide",
|
||||
"ext_gallery_install_restart_required",
|
||||
"ext_gallery_install_followup",
|
||||
]
|
||||
missing = {
|
||||
name: [key for key in required_keys if key not in block]
|
||||
for name, block in blocks.items()
|
||||
}
|
||||
missing = {name: keys for name, keys in missing.items() if keys}
|
||||
assert not missing, f"Locale(s) missing extension i18n key(s): {missing}"
|
||||
|
||||
|
||||
def test_extensions_post_install_i18n_is_localized_outside_english():
|
||||
blocks = _locale_blocks()
|
||||
english = blocks["en"]
|
||||
post_install_keys = [
|
||||
"ext_gallery_next_step",
|
||||
"ext_gallery_after_install",
|
||||
"ext_gallery_local_component_required",
|
||||
"ext_gallery_local_app_label",
|
||||
"ext_gallery_required_suffix",
|
||||
"ext_gallery_sidecar_required",
|
||||
"ext_gallery_native_host_required",
|
||||
"ext_gallery_open_setup_guide",
|
||||
"ext_gallery_install_restart_required",
|
||||
"ext_gallery_install_followup",
|
||||
]
|
||||
english_values = {key: _locale_string(english, key) for key in post_install_keys}
|
||||
untranslated = {
|
||||
name: [
|
||||
key
|
||||
for key in post_install_keys
|
||||
if _locale_string(block, key) == english_values[key]
|
||||
]
|
||||
for name, block in blocks.items()
|
||||
if name != "en"
|
||||
}
|
||||
untranslated = {name: keys for name, keys in untranslated.items() if keys}
|
||||
|
||||
assert not untranslated, f"Locale(s) keep English post-install guidance: {untranslated}"
|
||||
for name, block in blocks.items():
|
||||
assert "{0}" in _locale_string(block, "ext_gallery_required_suffix"), (
|
||||
f"{name} must preserve the local-app label placeholder"
|
||||
)
|
||||
|
||||
|
||||
def test_extensions_i18n_does_not_include_replacement_characters():
|
||||
assert "\ufffd" not in I18N_JS
|
||||
|
||||
|
||||
def test_extensions_docs_mentions_settings_panel_without_install_or_proxy_claims():
|
||||
|
|
|
|||
Loading…
Reference in a new issue