feat: add host-local custom project icons (#2335)

* feat(projects): add local icons and colors

* refactor(projects): simplify appearance handling

* fix(projects): serialize appearance saves

* test(app): update project appearance expectations

* test: fix project appearance CI expectations

* refactor(projects): organize appearance code

* fix(server): adopt main's reconcile model in project appearance updates

* fix(projects): preserve host-local appearance targets

* fix(desktop): restore Linux development startup

Ubuntu can block Electron's unprivileged user-namespace fallback while the npm-installed sandbox helper is not SUID. Disable the Chromium process sandbox only in the Linux development runner so packaged desktop policy remains unchanged.

* feat(projects): add custom project icons

Keep URL acquisition in the client so the host daemon only validates uploaded bytes. Project name and icon changes share one adaptive edit sheet.

* refactor(settings): scope projects by host

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
This commit is contained in:
nikuscs
2026-08-01 01:34:42 +08:00
committed by GitHub
co-authored by Mohamed Boudra
parent 706ae3d9bb
commit 800bd2bef1
67 changed files with 2158 additions and 912 deletions
+12 -12
View File
@@ -1621,6 +1621,7 @@ export const ar: TranslationResources = {
vimHint: "تنطبق على ملفات المصدر في الويب وسطح المكتب.",
},
hostSections: {
projects: "المشاريع",
connections: "اتصالات",
agents: "Agents",
workspaces: "Workspaces",
@@ -2121,24 +2122,23 @@ export const ar: TranslationResources = {
},
},
project: {
noEditableTarget: "ليس لدينا نسخة قابلة للتحرير من هذا المشروع على أي مضيف متصل.",
noEditableTarget: "هذا المشروع غير متاح للتحرير على هذا المضيف.",
backToProjects: "العودة إلى المشاريع",
switchHost: "تبديل المضيف",
rename: {
renamedToast: "تمت إعادة تسمية المشروع",
errorFallback: "تعذرت إعادة تسمية المشروع",
renameLabel: "إعادة تسمية المشروع",
resetLabel: "إعادة تعيين اسم المشروع إلى الافتراضي",
projectNameLabel: "اسم المشروع",
saveLabel: "احفظ اسم المشروع",
cancelLabel: "إلغاء إعادة التسمية",
reset: "إعادة ضبط",
edit: {
title: "تعديل المشروع",
name: "الاسم",
nameLabel: "اسم المشروع",
icon: "الأيقونة",
chooseImage: "اختيار صورة",
useAutomatic: "استخدام التلقائي",
imageUrl: "رابط صورة أو موقع",
save: "حفظ التغييرات",
savedToast: "تم تحديث المشروع",
},
readFailures: {
invalidTitle: "تعذر تحليل paseo.json",
invalidDescription: "قم بإصلاح الملف على القرص، ثم أعد تحميله.",
missingTitle: "هذا المضيف ليس لديه هذا المشروع",
missingWithHosts: "قم بالتبديل إلى مضيف آخر أعلاه، أو أعد التحميل.",
missingSingleHost: "المضيف المحدد ليس لديه سجل لهذا المشروع.",
transportTitle: "تعذر تحميل paseo.json",
transportFallback: "المضيف لم يستجب.",
+12 -12
View File
@@ -1632,6 +1632,7 @@ export const en = {
vimHint: "Applies to source files on web and desktop.",
},
hostSections: {
projects: "Projects",
connections: "Connections",
agents: "Agents",
workspaces: "Workspaces",
@@ -2133,24 +2134,23 @@ export const en = {
},
},
project: {
noEditableTarget: "We don't have an editable copy of this project on any connected host.",
noEditableTarget: "This project isn't editable on this host.",
backToProjects: "Back to projects",
switchHost: "Switch host",
rename: {
renamedToast: "Project renamed",
errorFallback: "Couldn't rename project",
renameLabel: "Rename project",
resetLabel: "Reset project name to default",
projectNameLabel: "Project name",
saveLabel: "Save project name",
cancelLabel: "Cancel renaming",
reset: "Reset",
edit: {
title: "Edit project",
name: "Name",
nameLabel: "Project name",
icon: "Icon",
chooseImage: "Choose image",
useAutomatic: "Use automatic",
imageUrl: "Image or website URL",
save: "Save changes",
savedToast: "Project updated",
},
readFailures: {
invalidTitle: "paseo.json couldn't be parsed",
invalidDescription: "Fix the file on disk, then reload.",
missingTitle: "This host doesn't have this project",
missingWithHosts: "Switch to another host above, or reload.",
missingSingleHost: "The selected host has no record of this project.",
transportTitle: "Couldn't load paseo.json",
transportFallback: "The host didn't respond.",
+12 -12
View File
@@ -1664,6 +1664,7 @@ export const es: TranslationResources = {
vimHint: "Se aplica a archivos fuente en web y escritorio.",
},
hostSections: {
projects: "Proyectos",
connections: "Conexiones",
agents: "Agents",
workspaces: "Workspaces",
@@ -2173,24 +2174,23 @@ export const es: TranslationResources = {
},
},
project: {
noEditableTarget: "No tenemos una copia editable de este proyecto en ningún host conectado.",
noEditableTarget: "Este proyecto no se puede editar en este host.",
backToProjects: "Volver a proyectos",
switchHost: "Cambiar de anfitrión",
rename: {
renamedToast: "Proyecto renombrado",
errorFallback: "No se pudo cambiar el nombre del proyecto",
renameLabel: "Cambiar nombre del proyecto",
resetLabel: "Restablecer el nombre del proyecto al valor predeterminado",
projectNameLabel: "Nombre del proyecto",
saveLabel: "Guardar nombre del proyecto",
cancelLabel: "Cancelar cambio de nombre",
reset: "Reiniciar",
edit: {
title: "Editar proyecto",
name: "Nombre",
nameLabel: "Nombre del proyecto",
icon: "Icono",
chooseImage: "Elegir imagen",
useAutomatic: "Usar automático",
imageUrl: "URL de imagen o sitio web",
save: "Guardar cambios",
savedToast: "Proyecto actualizado",
},
readFailures: {
invalidTitle: "paseo.json no se pudo analizar",
invalidDescription: "Fije el archivo en el disco y luego vuelva a cargarlo.",
missingTitle: "Este anfitrión no tiene este proyecto",
missingWithHosts: "Cambie a otro host de arriba o vuelva a cargar.",
missingSingleHost: "El anfitrión seleccionado no tiene registro de este proyecto.",
transportTitle: "No se pudo cargar paseo.json",
transportFallback: "El anfitrión no respondió.",
+12 -13
View File
@@ -1668,6 +1668,7 @@ export const fr: TranslationResources = {
vimHint: "S’applique aux fichiers source sur le web et le bureau.",
},
hostSections: {
projects: "Projets",
connections: "Relations",
agents: "Agents",
workspaces: "Workspaces",
@@ -2177,25 +2178,23 @@ export const fr: TranslationResources = {
},
},
project: {
noEditableTarget:
"Nous n'avons pas de copie modifiable de ce projet sur aucun hôte connecté.",
noEditableTarget: "Ce projet n'est pas modifiable sur cet hôte.",
backToProjects: "Retour aux projets",
switchHost: "Changer d'hôte",
rename: {
renamedToast: "Projet renommé",
errorFallback: "Impossible de renommer le projet",
renameLabel: "Renommer le projet",
resetLabel: "Réinitialiser le nom du projet par défaut",
projectNameLabel: "Nom du projet",
saveLabel: "Enregistrer le nom du projet",
cancelLabel: "Annuler le changement de nom",
reset: "Réinitialiser",
edit: {
title: "Modifier le projet",
name: "Nom",
nameLabel: "Nom du projet",
icon: "Icône",
chooseImage: "Choisir une image",
useAutomatic: "Utiliser l'automatique",
imageUrl: "URL d'image ou de site",
save: "Enregistrer",
savedToast: "Projet mis à jour",
},
readFailures: {
invalidTitle: "paseo.json n'a pas pu être analysé",
invalidDescription: "Corrigez le fichier sur le disque, puis rechargez.",
missingTitle: "Cet hôte n'a pas ce projet",
missingWithHosts: "Basculez vers un autre hôte ci-dessus ou rechargez.",
missingSingleHost: "L'hôte sélectionné n'a aucune trace de ce projet.",
transportTitle: "Impossible de charger paseo.json",
transportFallback: "L'hôte n'a pas répondu.",
+12 -13
View File
@@ -1637,6 +1637,7 @@ export const ja: TranslationResources = {
vimHint: "Web とデスクトップのソースファイルに適用されます。",
},
hostSections: {
projects: "プロジェクト",
connections: "接続",
agents: "エージェント",
workspaces: "ワークスペース",
@@ -2143,25 +2144,23 @@ export const ja: TranslationResources = {
},
},
project: {
noEditableTarget:
"接続されているホストのどれにもこのプロジェクトの編集可能なコピーがありません。",
noEditableTarget: "このホストではこのプロジェクトを編集できません。",
backToProjects: "プロジェクトに戻る",
switchHost: "ホストを切り替え",
rename: {
renamedToast: "プロジェクト名を変更しました",
errorFallback: "プロジェクト名を変更できませんでした",
renameLabel: "プロジェクトの名前を変更",
resetLabel: "プロジェクト名をデフォルトにリセット",
projectNameLabel: "プロジェクト名",
saveLabel: "プロジェクト名を保存",
cancelLabel: "名前変更をキャンセル",
reset: "リセット",
edit: {
title: "プロジェクトを編集",
name: "名前",
nameLabel: "プロジェクト名",
icon: "アイコン",
chooseImage: "画像を選択",
useAutomatic: "自動を使う",
imageUrl: "画像またはサイトのURL",
save: "変更を保存",
savedToast: "プロジェクトを更新しました",
},
readFailures: {
invalidTitle: "paseo.jsonを解析できませんでした",
invalidDescription: "ディスク上のファイルを修正してから再読み込みしてください。",
missingTitle: "このホストにはこのプロジェクトがありません",
missingWithHosts: "上で別のホストに切り替えるか、再読み込みしてください。",
missingSingleHost: "選択したホストにはこのプロジェクトの記録がありません。",
transportTitle: "paseo.jsonを読み込めませんでした",
transportFallback: "ホストが応答しませんでした。",
+12 -12
View File
@@ -1650,6 +1650,7 @@ export const ptBR: TranslationResources = {
vimHint: "Aplica-se a arquivos-fonte na web e no desktop.",
},
hostSections: {
projects: "Projetos",
connections: "Conexões",
agents: "Agentes",
workspaces: "Workspaces",
@@ -2158,24 +2159,23 @@ export const ptBR: TranslationResources = {
},
},
project: {
noEditableTarget: "Não há uma cópia editável deste projeto em nenhum host conectado.",
noEditableTarget: "Este projeto não pode ser editado neste host.",
backToProjects: "Voltar para projetos",
switchHost: "Trocar host",
rename: {
renamedToast: "Projeto renomeado",
errorFallback: "Não foi possível renomear projeto",
renameLabel: "Renomear projeto",
resetLabel: "Redefinir nome do projeto para o padrão",
projectNameLabel: "Nome do projeto",
saveLabel: "Salvar nome do projeto",
cancelLabel: "Cancelar renomeação",
reset: "Redefinir",
edit: {
title: "Editar projeto",
name: "Nome",
nameLabel: "Nome do projeto",
icon: "Ícone",
chooseImage: "Escolher imagem",
useAutomatic: "Usar automático",
imageUrl: "URL de imagem ou site",
save: "Salvar alterações",
savedToast: "Projeto atualizado",
},
readFailures: {
invalidTitle: "Não foi possível analisar paseo.json",
invalidDescription: "Corrija o arquivo no disco e recarregue.",
missingTitle: "Este host não tem este projeto",
missingWithHosts: "Troque para outro host acima ou recarregue.",
missingSingleHost: "O host selecionado não tem registro deste projeto.",
transportTitle: "Não foi possível carregar paseo.json",
transportFallback: "O host não respondeu.",
+12 -13
View File
@@ -1655,6 +1655,7 @@ export const ru: TranslationResources = {
vimHint: "Применяется к исходным файлам в веб- и настольной версии.",
},
hostSections: {
projects: "Проекты",
connections: "Соединения",
agents: "Agents",
workspaces: "Workspaces",
@@ -2161,25 +2162,23 @@ export const ru: TranslationResources = {
},
},
project: {
noEditableTarget:
"У нас нет редактируемой копии этого проекта ни на одном подключенном хосте.",
noEditableTarget: "Этот проект нельзя редактировать на этом хосте.",
backToProjects: "Вернуться к проектам",
switchHost: "Сменить хост",
rename: {
renamedToast: "Проект переименован",
errorFallback: "Не удалось переименовать проект",
renameLabel: "Переименовать проект",
resetLabel: "Сбросить имя проекта по умолчанию",
projectNameLabel: "Название проекта",
saveLabel: "Сохранить название проекта",
cancelLabel: "Отменить переименование",
reset: "Перезагрузить",
edit: {
title: "Изменить проект",
name: "Название",
nameLabel: "Название проекта",
icon: "Значок",
chooseImage: "Выбрать изображение",
useAutomatic: "Использовать автоматический",
imageUrl: "URL изображения или сайта",
save: "Сохранить изменения",
savedToast: "Проект обновлён",
},
readFailures: {
invalidTitle: "paseo.json не удалось разобрать",
invalidDescription: "Исправьте файл на диске, затем перезагрузите.",
missingTitle: "У этого хоста нет этого проекта",
missingWithHosts: "Переключитесь на другой хост выше или перезагрузите компьютер.",
missingSingleHost: "У выбранного хоста нет записей об этом проекте.",
transportTitle: "Не удалось загрузить paseo.json.",
transportFallback: "Хозяин не ответил.",
+12 -12
View File
@@ -1602,6 +1602,7 @@ export const zhCN: TranslationResources = {
vimHint: "适用于网页和桌面端的源文件。",
},
hostSections: {
projects: "项目",
connections: "连接",
agents: "Agents",
workspaces: "Workspaces",
@@ -2095,24 +2096,23 @@ export const zhCN: TranslationResources = {
},
},
project: {
noEditableTarget: "任何已连接 Host 上都没有这个 Project 的可编辑副本。",
noEditableTarget: "此项目无法在这个 Host 上编辑。",
backToProjects: "返回 Projects",
switchHost: "切换 Host",
rename: {
renamedToast: "Project 已重命名",
errorFallback: "无法重命名 Project",
renameLabel: "重命名 Project",
resetLabel: "将 Project 名称重置为默认值",
projectNameLabel: "Project 名称",
saveLabel: "保存 Project 名称",
cancelLabel: "取消重命名",
reset: "重置",
edit: {
title: "编辑 Project",
name: "名称",
nameLabel: "Project 名称",
icon: "图标",
chooseImage: "选择图片",
useAutomatic: "使用自动",
imageUrl: "图片或网站 URL",
save: "保存更改",
savedToast: "Project 已更新",
},
readFailures: {
invalidTitle: "无法解析 paseo.json",
invalidDescription: "修复磁盘上的文件,然后重新加载。",
missingTitle: "这个 Host 没有这个 Project",
missingWithHosts: "切换到上方其他 Host,或重新加载。",
missingSingleHost: "所选 Host 没有这个 Project 的记录。",
transportTitle: "无法加载 paseo.json",
transportFallback: "Host 没有响应。",