Registre du réalisé

Annexe de Roadmap (option registre-livrés, A-8) — chaque entrée est citée par sa ligne de renvoi dans la roadmap active. Dernière revue : 2026-08-08.

Ce registre ne porte que l'état des chantiers déjà livrés, le plus récent en tête : leur fiche est reprise telle qu'elle était dans la roadmap au moment de la bascule, suivie d'une ligne de métadonnées (journal, pièges, commits). Ce qui reste à faire est dans la roadmap active — y compris ce qu'un chantier livré laisse ouvert, rassemblé dans sa section « Reste ouvert » — et l'événement daté de chaque livraison est dans le journal. Les évolutions post-livraison s'ajoutent ici en une ligne chacune ; dès qu'une évolution touche l'architecture, une convention ou plus d'un fichier, elle rouvre un chantier dans l'actif.

Durcir l'import de dossier collé (défaut de sécurité du code livré) ✅ Corrigé (2026-08-08)

Trouvé en instruisant l'idée « Team Sharing » de la réflexion post-V1.0, et sans rapport avec elle : le défaut est dans le chemin déjà livré et publié du collage de dossier (src/groupShare.ts, chantier Partage rapide de groupe). Trois trous, vérifiés dans le code, non exercés sur un banc.

Une icône collée n'est pas sanitisée, et une icône est rendue par innerHTML brut

sanitiseGroup() fait out.icon = asString(raw.icon) (groupShare.ts:439 pour le dossier, :451 pour chaque profil) — un simple contrôle de type. Or <profile-icon> de Tabby teste icon.startsWith('<') puis délègue à FastHtmlBindDirective, qui fait innerHTML = value : c'est exactement le court-circuit du DomSanitizer que svgSanitizer.ts documente déjà en tête de fichier. Un "icon": "<img src=x onerror=…>" dans un JSON collé s'exécute donc au moment où l'arbre se peint, dans un renderer où Node est disponible — ce plugin appelle lui-même require('child_process') (editorLauncher.service.ts), ce qui suffit à établir la portée. workspaceShare.ts:162 fait déjà bien : il passe par sanitiseIcon()sanitizeSvgIcon() (DOMPurify) et plafonne une classe Iconify. C'est la même discipline qui manque ici, et l'asymétrie entre les deux fichiers est le signal qu'il fallait lire.

À faire avant d'y toucher : un banc de charges hostiles (icône avec gestionnaire d'événement, profil local avec command, profil SSH avec proxyCommand) collées sur des entrées jetables grp-zzz-test-*, pour que le comptage tranche plutôt que la relecture — c'est la leçon du piège #77, où un champ était sorti entier d'une purge que tout le monde croyait complète. Vérifier aussi si defaults.<fournisseur> hérite des mêmes trous (il passe par purgeDefaults(), donc a priori oui).

Corrigé le 2026-08-08, par un agent en worktree isolé puis relu et revalidé. Le correctif ne colmate pas les trois trous, il retourne la logique : la purge passe d'une liste noire à une liste blanche d'options par type de profil (PROFILE_OPTION_WHITELISTS), établie en lisant les interfaces réelles de Tabby pour ssh, telnet et serial. C'est ce qui referme la classe entière de défauts : une liste noire rouvrait le problème en silence à chaque option nouvelle de l'hôte. sanitiseIcon() est sorti dans src/iconSanitize.ts, partagé avec workspaceShare.ts au lieu d'être dupliqué — c'est l'asymétrie entre les deux fichiers qui avait laissé le trou.

Le type local ne voyage plus du tout — décision de l'utilisateur, 2026-08-08

SessionOptions (command, args, cwd, env, runAsAdministrator) n'a aucun sous-ensemble sûr : tout y est un vecteur d'exécution. Le type est donc absent de la liste blanche, et un profil ou un bloc defaults.local est rejeté entièrement, compté, à l'export comme à l'import — y compris entre les machines de l'auteur au niveau secrets. Perte de fonctionnalité assumée : un profil local décrit une commande à exécuter, ce qui est exactement ce qu'on refuse de recevoir par presse-papiers. L'alternative (le laisser voyager vidé de son exécution, donc inerte à l'arrivée) a été proposée et écartée.

Banc de vérification — 13 charges hostiles sur 13, plus la rétrocompatibilité. C'est le comptage qui a tranché, pas la relecture, comme l'exigeait la leçon du piège #77 : icône <img onerror> rejetée entièrement (racine non-<svg>), icône <svg onload> conservée sans son gestionnaire, profil local et type inconnu rejetés, defaults.local.command écarté en bloc, defaults.ssh.scripts retiré en laissant keepaliveInterval, pointeur vault:// retiré en gardant le chemin local, profondeur 30 ramenée à 20, payload de plus de 4 Mio refusé. Un export d'avant le correctif reste lisible et se retrouve simplement purgé plus sévèrement, l'écart apparaissant dans strippedOnImport — donc dans la notice rouge. Version du format inchangée : les trois nouveaux décomptes (commands, unknownOptions, rejectedTypes) sont additifs et se lisent à 0 sur un ancien payload.

Corrigé le 2026-08-08 · Journal : « Durcir l'import de dossier collé » (2026-08-08) · Commits : cddbbc0

Passer les popups riches en modales centrées ✅ Livré (2026-08-08)

Constaté en construisant la modale des snippets, et non demandé : les tunnels et le sélecteur d'icônes souffrent du même mal — un formulaire dans une boîte ancrée au curseur, large de 300 px — et portent chacun leur propre positionnement, leur propre recadrage à l'écran et leur propre exception au clic extérieur. Trois mécaniques dupliquées pour un motif qui a désormais un patron éprouvé (snippetsModal, noteModal, pasteGroupModal).

Les menus contextuels, eux, restent des menus : ils sont courts et doivent s'ouvrir sous la souris. Ce chantier vise les popups qui contiennent un formulaire, pas ceux qui contiennent une liste d'actions — la distinction est ce qui l'empêche de déborder sur le chantier voisin.

Non ouvert pendant la phase 0.3.0 : la pile de code non testé était déjà large.

Livré le 2026-08-08, par un agent en worktree isolé puis relu et revalidé. Deux composants créés sur le patron des trois modales existantes : TunnelsModalComponent et IconPickerModalComponent. Le petit menu de bascule favori du picker (clic droit sur une tuile) a migré à l'intérieur de la modale avec son propre gestionnaire de clic, au lieu de vivre dans le HostListener de l'arbre. formatTunnel()/tunnelKey() sont sortis dans src/tunnels.ts, seul point resté commun avec la section « Tunnels actifs » de l'arbre — qui, elle, affiche les tunnels réellement montés et n'était pas du périmètre.

Le résultat se mesure au nettoyage, pas à l'ergonomie : sidebarTree.component.{ts,pug,scss} perd 758 lignes nettes — vingt-six méthodes et une douzaine de champs partis dans les modales, contextMenuMode délesté de ses valeurs icon/tunnels, .icon-picker retiré des deux listes closest() du HostListener et du sélecteur de recadrage, blocs SCSS du picker et de la popup tunnels supprimés. C'était l'objet de la fiche : faire disparaître trois mécaniques dupliquées, pas en ajouter une quatrième. Aucune clé i18n ajoutée ni retirée — les chaînes déplacées ont gardé les leurs.

Laissé en popup délibérément, sur le « au cas par cas » que la fiche autorise : renommage, création de dossier/profil, confirmation de suppression, menu et couleur de workspace. Aucune ne porte de formulaire à plusieurs champs et toutes tiennent sous la souris sans recadrage notable. Le chargement différé des collections d'icônes est préservé : loadIconEntries() n'est appelé que depuis onIconQueryChange(), donc la modale ne charge rien à l'ouverture.

Livré le 2026-08-08 · Journal : « Popups à formulaire en modales centrées » (2026-08-08) · Commits : 634496b

Relevé par l'utilisateur le 2026-08-03, au premier test de la passe du partage de groupe : « il s'étend vers l'infini et devient de moins en moins utilisable ». À cadrer avec lui avant d'y toucher.

Le menu du dossier porte treize entrées : déplacer la sélection, lancer les sessions, snippets, note, favori, renommer, nouveau dossier, nouveau profil, icône, les deux copies, cacher dans le workspace, supprimer. Les deux dernières arrivées sont celles du partage de groupe, ce qui rend la remarque d'autant plus juste.

Piste évoquée par l'utilisateur : un « afficher plus » ou équivalent.

Ce qui est à trancher n'est pas la mécanique

C'est le critère de partage — ce qui mérite le premier niveau. Deux angles possibles, et ils ne donnent pas la même liste : la fréquence d'usage, ou la nature du geste (agir sur le dossier / créer dedans / le partager / le retirer de la vue). À poser comme une question, pas à deviner : trois des cinq items de G1 se sont révélés autres qu'annoncés faute de l'avoir fait.

Noter aussi que le menu du profil grossit en parallèle, et qu'un choix fait ici le concerne. Et que le clic droit doit rester court et s'ouvrir sous la souris : c'est une raison de plus de ne pas y régler ce qui relève de l'onglet de réglages.

Tranché et livré le 2026-08-08. Le critère retenu n'est aucun des deux angles que la fiche opposait (fréquence d'usage / nature du geste) : l'utilisateur a demandé des sous-menus nommés, à la façon des menus contextuels Windows — plusieurs groupes titrés plutôt qu'un unique « Plus… ». Poser la question plutôt que deviner a donc bien payé une seconde fois. Découpage appliqué aux deux menus dans le même geste, comme la fiche le recommandait :

Sous-menus latéraux (choix utilisateur), ouverts au survol comme au clic, avec fermeture différée de 180 ms pour que le trajet diagonal du parent vers le panneau ne les referme pas. Le panneau est monté en frère du menu, jamais en descendant : une ancre dans une ancre est désimbriquée en silence par le parseur (piège #38). Il bascule à gauche quand le bord droit manque, et passe par le clampInViewport() unique du plugin — donc il ne recouvre pas non plus le pied de page. .group-context-submenu a été ajouté aux deux tests closest() du HostListener('document:click') : sans cela, ouvrir un sous-menu refermait tout le menu (piège #15).

Livré le 2026-08-08 · Journal : « Menus contextuels en sous-menus nommés » (2026-08-08) · Pièges rejoués : #15, #38 · Commits : 145d4eb

Icônes dashboard-icons (logos de services IT) ✅ Livré (2026-08-08)

Ajouter homarr-labs/dashboard-icons (~1 800 logos de services auto-hébergés — Proxmox, Pi-hole, Nextcloud… — SVG source, variantes -light/-dark, manifeste metadata.json, Apache-2.0 avec disclaimer sur les marques) comme troisième source de la recherche unifiée du picker, aux côtés de FontAwesome et des collections Iconify — bien plus parlant pour nommer les profils SSH d'un parc que des glyphes génériques. Pas de paquet npm : vendoring depuis le dépôt GitHub par un script de génération (JSON nom → corps SVG + variantes, alias de recherche tirés de metadata.json), avec PROVENANCE.md et entrée dans THIRD-PARTY-NOTICES.md.

Poids, mesuré le 2026-08-08 (API GitHub, arbre main) : le svg/ amont fait 3 295 fichiers pour 40,5 Mo bruts, mais la médiane est à 1,9 Ko — 44 fichiers aberrants (du bitmap incrusté dans du SVG, jusqu'à 4 Mo pour dilg.svg) portent à eux seuls 26,7 Mo. Plafonné à 50 Ko par fichier : 3 215 icônes (98,7 %) pour 11,4 Mo bruts, soit ~3-4 Mo gzip estimés dans le tarball. Référence actuelle : le chunk icon-sets.js (MDI + Tabler) pèse 5,1 Mo bruts / ~1,1 Mo gzip. Cumul des trois sources avec le plafond : ~16,5 Mo bruts embarqués, ~4,5 Mo compressés côté icônes.

Réalisé le 2026-08-08, conforme aux trois arbitrages ci-dessus. Chiffres constatés à la génération (amont d64ee64) : 3 294 fichiers lus, 80 exclus par le plafond de 50 Ko (29,3 Mo à eux seuls) et 8 à racine SVG non standard, soit 3 206 fichiers regroupés en 2 468 icônes logiques dont 665 à plusieurs variantes. src/dashboardIcons.json pèse 12,0 Mo, le chunk dashboard-icons.js 12,8 Mo bruts (~3,3 Mo gzip), chargé à la première recherche seulement. Le regroupement des variantes se fait par convention de nom de fichier lue dans svg/, pas par le champ colors de metadata.json — celui-ci ne couvre qu'une minorité des paires réellement présentes ; le manifeste ne sert qu'aux alias de recherche. Variantes proposées en pastilles au coin de chaque tuile, en clic gauche : le clic droit reste l'épinglage en favori, jamais intercepté.

Une icône injectée par innerHTML n'est ni un fichier isolé ni un shadow root — d'où trois isolations à la génération

Le rendu passe par FastHtmlBindDirective (element.innerHTML = value), qui dépose le balisage dans le document vivant. Trois conséquences, toutes corrigées une fois au vendoring et non à l'exécution (voir scripts/vendor-dashboard-icons.js) :

Corrigé dans le même geste, même cause de fond — un popup flottant ignore la mise en page : le picker, position: fixed sans borne de hauteur, couvrait sur une fenêtre courte la sidebar entière, onglets et pied de page compris, en flottant à z-index: 100 par-dessus le pied de page que le flux garde pourtant inviolable. Il est borné (max-height = fenêtre moins la bande du pied de page) et défile en interne, et clampInViewport() réserve désormais cette même bande pour tous les popups du plugin — SIDEBAR_FOOTER_INSET (src/viewport.ts) et $sidebar-footer-height (SCSS) sont faits pour bouger ensemble.

Reste ouvert : la cadence de rafraîchissement du vendoring n'est pas décidée (le dépôt amont bouge vite) — le script se relance à la demande et réécrit PROVENANCE.md avec le commit amont du jour.

Livré le 2026-08-08 · Journal : « Icônes dashboard-icons » (2026-08-08) · Pièges issus du chantier : l'encadré ci-dessus (isolation d'un SVG injecté par innerHTML) · Commits : 89838b0, 93bab1c, ea02170

Internationalisation (i18n) ✅ Livré (2026-08-08)

Greffée sur le ngx-translate déjà chargé par tabby-core, plutôt qu'une dépendance i18n séparée — @ngx-translate/core reste external dans la config webpack (fourni par l'hôte, pas embarqué). La chaîne source anglaise EST la clé (pas de table d'ids courts) : chaque appel {{ '…' | translate }} porte directement le texte anglais par défaut, ce qui rend le rendu correct même sans traduction chargée.

Le 3ᵉ argument de setTranslation() est impératif

translate.setTranslation(lang, table, true) — le troisième argument shouldMerge doit être true. Sans lui, setTranslation() remplace toute la table de la langue au lieu de l'étendre, ce qui écraserait les traductions déjà posées par Tabby lui-même pour cette langue.

Piège appris — une apostrophe droite dans une expression inline casse le JIT au runtime, invisible au build

Lier une chaîne anglaise à apostrophe droite dans une expression Angular inline ({{ '…' | translate }}, [title]="'…' | translate") casse la compilation JIT au runtime — l'apostrophe termine prématurément le littéral de chaîne. npm run build (webpack/tsc) ne le voit jamais, puisqu'il ne fait pas tourner le JIT d'Angular. C'est ce qui a cassé l'onglet de réglages pendant le lot 2 : il a été restauré en français statique (attributs title="…" statiques, hors i18n) plutôt que réparé sur place. Sa ré-internationalisation propre devra passer par des propriétés de composant (get x() { return this.i18n.t('…') }) liées via [title]="x", jamais par un littéral inline. Les tirets cadratins des clés anglaises ont par prudence été remplacés par : — les valeurs FR/ES/DE, elles, gardent leur tiret : elles vivent dans les fichiers .ts et ne passent jamais par le JIT.

Livré le 2026-08-08 · Journal : « i18n lot 3 » et « i18n lot 4 » (2026-08-08) ; socle et lots 1-2 dans « G2, G3 et G4 — la phase 0.3.0 close » (2026-08-08) · Pièges issus du chantier : le piège JIT ci-dessus (encadré) · Commits : e96ee51 (phase 0.3.0 : socle + lots 1-2), ee07b54, dbdcf21, 32c8da2, 9810173 (les hashs de phase sont partagés avec ses autres chantiers)

SFTP contextuel

Bascule Profils/SFTP dans l'espace de la sidebar elle-même (pas un panneau séparé ancré à droite, façon FileZilla — plan initial abandonné). Volet fichiers locaux masqué par défaut. Déclenché depuis un onglet SSH déjà connecté (pas de session headless dédiée). SFTPPanelComponent/SFTPSession/SFTPFile confirmés exportés par tabby-ssh — pas de réimplémentation locale nécessaire, contrairement à profile-tree en son temps.

Fait — testé et validé par l'utilisateur le 2026-07-29

Branche feat/sftp-panel, 11 commits, détail dans AI-HISTORY (2026-07-29). Le panneau natif est sous-classé (SidebarPlusSftpBrowserComponent) : toute la logique SFTP est héritée, seule la template est à nous — nécessaire dès qu'on a voulu maîtriser le rendu des lignes (date, permissions, infobulles, alignement), tout cela étant du contenu de template hors d'atteinte du CSS. Six pièges consignés au passage (#32 à #36 et un complément au #6), dont deux vrais bugs de Tabby sous Windows.

Question ouverte tranchée : la bascule est globale, pas par onglet. Choix de cette première passe, à rejuger à l'usage — l'état par onglet est un problème de gestion de modes qui n'avait pas à être résolu pour prouver le reste.

Livré

Livré (2026-08-08)

Limites connues du renvoi de fichier édité ✅ Levées (2026-08-01)

Ce que ce bloc décrivait, et qui n'a plus cours

Les deux limites listées ici à la livraison du chantier ont été traitées par Transferts par l'API officielle — elles sont conservées pour que la trace de ce qui a été corrigé reste lisible, pas parce qu'elles décrivent l'état actuel :

Pas de détection de conflit : si le fichier distant changeait entre le téléchargement et un enregistrement, l'enregistrement l'écrasait. → Levée par le point 2, qui compare l'empreinte distante avant tout envoi et ouvre une modale de conflit.

Nettoyage des copies temporaires à la destruction du panneau, pas à la fermeture de l'éditeur. → Levée par le point 3 : purge au démarrage de tout ce qu'un run antérieur a laissé, et purge du dossier de la fenêtre à sa fermeture.

Écarté : SCP

Question posée par l'utilisateur au lancement du chantier. Non, et pas pour des raisons de performance : le transport SSH de Tabby est russh, dont lib/ n'expose que sftp.js — aucune surface SCP (les seules occurrences de « SCP » dans tabby-ssh sont le réglage lançant WinSCP en externe). Il faudrait réimplémenter le protocole sur un canal exec brut. Et l'avantage supposé ne porte pas : SCP ne gagne que sur le streaming d'un gros fichier unique, et n'a ni readdir, ni stat, ni rename, ni unlink — soit l'intégralité du métier d'un navigateur de fichiers. Il faudrait faire tourner SFTP pour la navigation de toute façon.

Livré le 2026-08-08 · Journal : « SFTP contextuel dans la sidebar (2026-07-29) » (2026-07-29), « G2, G3 et G4 — la phase 0.3.0 close, une passe de test presque tout entière pilotée, et un socle d'i18n (2026-08-08) » (2026-08-08) · Pièges issus du chantier : #32, #33, #34, #35, #36 · Commits : c3ee956, c2732e5, a22f2eb, 6c5c74d, 85754d8, 4460ab1, 2d06daf, 5551483, 5af7c12, e8a0024, c3ea59c, d284e62 (commits du lot de livraison, partagés entre ses chantiers)

Workspaces

Plusieurs espaces de travail nommés (ex. perso / pro / projet X) filtrant la visibilité des groupes/profils affichés. Pas de mécanisme de masquage séparé : masquer un profil/groupe = le décocher du workspace actif — les workspaces SONT le mécanisme de masquage. Traité avant le SFTP (initialement prévu après) — choix explicite de l'utilisateur le 2026-07-28, ordre inversé par rapport à la priorisation d'origine.

Fait — testé et validé par l'utilisateur le 2026-07-29

Passe de test manuel complète dans Tabby (onglets de workspace, masquage et panneau de restauration, comportement de la barre de filtre, favoris cloisonnés, ordre cloisonné, imbrication depuis un workspace, survie à un redémarrage complet). Cinq correctifs livrés à cette occasion, voir AI-HISTORY (2026-07-29) et les pièges #30/#31. Commit 42dcb09.

Complété — ✅ Fait (2026-08-08)

Les quatre items différés en 2026-07-29 (icône par workspace, sélecteur, couleurs contextuelles, export/import JSON) sont livrés, plus le retour visuel réordonner/imbriquer et le pied de sidebar — détail dans chaque puce ci-dessous et dans AI-HISTORY (2026-08-08). Construit par agents séquentiels sur la branche de phase, puis repris à la main sur deux retours utilisateur pour le sélecteur.

Propositions issues du test manuel du 2026-07-29 ✅ Tranchées (2026-08-07)

Consignées à la demande de l'utilisateur pour vérification de faisabilité. Arbitrées le 2026-08-07 : deux écartées en statu quo, une retenue pour construction (retour visuel réordonner/imbriquer, livré le 2026-08-08 — voir la fiche Workspaces).

Livré le 2026-08-08 · Journal : « Workspaces + déblocage complet du glisser-déposer de dossiers (2026-07-28) » (2026-07-28), « Test manuel des Workspaces : 5 correctifs (2026-07-29) » (2026-07-29), « G2, G3 et G4 — la phase 0.3.0 close, une passe de test presque tout entière pilotée, et un socle d'i18n (2026-08-08) » (2026-08-08) · Pièges issus du chantier : #22, #23, #24, #25, #26, #27, #28, #29, #30, #31 · Commits : 42dcb09, d284e62 (commits du lot de livraison, partagés entre ses chantiers)

Section "Sessions Actives" en haut de la sidebar

Zone pliable dédiée tout en haut de la barre latérale, listant uniquement les connexions SSH ouvertes en arrière-plan.

Note d'architecture

Bien que SCP ait été évoqué, le socle de Tabby (tabby-ssh) exporte déjà SFTPSession (voir point de veille AI-CONTEXT), il est donc impératif de s'appuyer sur SFTP pour garantir la stabilité globale.

Fait — testé et validé par l'utilisateur le 2026-07-29

Commits a652f0f (socle partagé) et 8b3dcfa (la section), détail dans AI-HISTORY. Une ligne par pane, pas par onglet ; clic = focus (y compris à l'intérieur d'un split) ; action de survol pour sauter à la session et basculer la sidebar sur son SFTP. Deux pièges consignés (#37, #38) et un bug préexistant du panneau SFTP corrigé au passage (son suivi de focus restait collé au premier split rencontré).

Trois décisions prises, à rejuger à l'usage : la section est masquée entièrement quand rien n'est connecté plutôt qu'affichée vide ; elle n'est pas cloisonnée par workspace (une session ouverte est un fait de l'app, pas du workspace regardé — la masquer serait un moyen de la perdre de vue) ; l'état replié va en localStorage, pas dans config.yaml.

Intégration visuelle livrée le 2026-08-08 ✅ Fait : les états de transfert SFTP (vitesse, file d'attente) s'affichent désormais aussi sur la ligne de session elle-même, pas seulement dans le panneau du gestionnaire (volet 1, en place depuis le 2026-08-02) qui reste la source de vérité.

Livré le 2026-08-08 · Journal : « Section « Sessions actives » (2026-07-29) » (2026-07-29), « G2, G3 et G4 — la phase 0.3.0 close, une passe de test presque tout entière pilotée, et un socle d'i18n (2026-08-08) » (2026-08-08) · Pièges issus du chantier : #37, #38 · Commits : a652f0f, 8b3dcfa, d284e62 (commits du lot de livraison, partagés entre ses chantiers)

Transferts SFTP : gestionnaire horodaté & glisser-déposer

Demandé par l'utilisateur le 2026-07-29, avec une urgence relevée : ce chantier passe devant Auto-Enter — depuis sorti du périmètre — et devient le prochain à construire. Il absorbe le « mini-gestionnaire de transferts » différé du SFTP contextuel et débloque au passage l'affichage des états de transfert attendu par Sessions actives. Les trois volets sont livrés le 2026-08-02. Deux retours de la passe manuelle du volet 3 restent ouverts (curseur d'interdit hors fenêtre, notice à rendre persistante), ainsi qu'une vérification demandée : ce que les plugins SFTP graphiques existants gèrent déjà de ces mécaniques.

Trois demandes distinctes, à traiter dans cet ordre — la première est autonome, les deux autres partagent leur plomberie de dépôt :

  1. Gestionnaire de transferts en bas de sidebar, masqué quand vide, chaque transfert horodaté.
  2. Dépôt depuis l'OS directement sur un dossier de la liste distante → envoi dans ce dossier, avec garde-fou : tout dépôt qui ne vise pas un dossier part dans le dossier actif.
  3. Glisser-déposer interne au panneau SFTP (déplacer un fichier/dossier distant dans un autre dossier distant), pour l'ergonomie.

1. Gestionnaire de transferts (bas de sidebar, horodaté) — ✅ Fait (2026-08-02)

Livré, avec deux écarts au plan ci-dessous

L'option (c) a été suivie : le panneau s'abonne à fileTransferStarted$ et reflète donc aussi les transferts du panneau natif et des autres plugins. Deux différences avec ce qui était écrit ici, toutes deux tranchées en test :

Le chemin distant et la session prévus dans la ligne n'y sont pas : la ligne est restée volontairement compacte, et l'information n'a pas été réclamée au test. À rouvrir si le besoin se manifeste — le registre les a sous la main pour nos propres transferts.

Le menu natif est masqué par un réglage coché par défaut, visant l'hôte du dropdown et transfers-menu — l'avertissement ci-dessous sur container='body' s'est vérifié tel quel.

Confirmation avant annulation — ✅ Fait (2026-08-08)

Retirer une ligne encore active depuis le bouton poubelle/balai du gestionnaire demande désormais confirmation, sur le même ConfirmModalComponent que le reste du plugin — la sémantique removeTransfer() annonçée plus haut (retirer une ligne active annule le transfert) restait vraie mais silencieuse. Non pilotable par CDP à la passe de test : le composant transferts est monté par Angular en mode production, donc inatteignable (window.ng n'expose que ɵcompilerFacade, __ngContext__ n'est qu'un entier vers un registre injoignable) — délégué à l'œil de l'utilisateur, qui l'a validé le 2026-08-08. Le mécanisme lui-même était déjà prouvé par ailleurs, dans la passe SFTP de la même vague.

Tranché le 2026-07-29 : ligne sobre, pas de barre de progression

Le design acté du SFTP contextuel avait explicitement écarté les barres de progression (« coût de re-render Angular trop élevé avec plusieurs transferts en parallèle »), au profit de flèches d'activité et d'un pourcentage au survol. Le transfers-menu natif, lui, utilise ngb-progressbar par transfert — recopier sa template aurait inversé cette décision sans le dire. Choix confirmé par l'utilisateur : ligne sobre (état + pourcentage textuel + vitesse). La barre reste possible plus tard si le rendu tient.

Réutiliser le transfers-menu natif : impossible tel quel, vérifié aux deux niveaux

Question posée par l'utilisateur le 2026-07-29 (« on peut pas juste piquer le transfers-menu natif ? »). Vérification faite selon piège #13 sur le paquet installé :

Reste donc la reprise de code (67 lignes de TS + 23 de pug, MIT — même pratique que profile-tree, à consigner dans THIRD-PARTY-NOTICES.md). Ce qui vaut d'être repris est sa logique, pas sa template : la sémantique de removeTransfer() (retirer une ligne encore active annule le transfert) et la confirmation « des transferts sont en cours, tout abandonner ? » de removeAll(). Sa template, elle, part sur la progressbar écartée ci-dessus, et n'a ni horodatage ni chemin distant — soit précisément ce qu'on ajoute.

Le menu de transferts natif s'ouvre déjà tout seul

Vérifié sur le source installé : startUploadFromDragEvent() publie chaque fichier dans fileTransferStarted$ (platform.ts:175), et appRoot.component.ts:172-174 y est abonné et appelle activeTransfersDropdown.open(). Donc tout dépôt dans la sidebar fait déjà apparaître le dropdown natif, et notre panneau en serait le doublon visuel. Deux issues, à arbitrer : (a) assumer le doublon et lier ce chantier à l'« UI trimming » des options & polissage UI pour masquer le bouton natif ; (b) ne pas passer par startUploadFromDragEvent() et construire nous-mêmes les FileUpload depuis le DataTransfer, ce qui coupe la publication mais nous met à la charge du parcours d'arborescence (webkitGetAsEntry).

Option (c), recommandée, issue de la question de l'utilisateur sur la reprise du menu natif : reprendre sa source de données plutôt que son composant. Notre panneau s'abonne au même fileTransferStarted$ que appRoot et devient le gestionnaire de transferts de toute l'application — il reflète alors aussi les transferts du panneau SFTP natif et des autres plugins, ce qui rend le menu natif purement redondant et justifie de le neutraliser. Tranche du même coup le point « périmètre » listé plus bas. À noter : un transfert capté par cet abonnement mais non lancé par nous n'a ni chemin distant ni session associée (FileTransfer ne les porte pas) — ces lignes seront donc moins détaillées, et le registre maison reste nécessaire pour nos propres transferts, notamment ceux de l'édition distante.

Masquer le bouton natif ne suffira pas

Le dropdown natif est déclaré avec container='body' (appRoot.component.pug:61-75) : son menu est téléporté dans <body>, hors du bouton. Une règle d'UI trimming visant le seul bouton laisserait donc le menu s'afficher quand même à chaque dépôt (positionné par Popper sur un toggle masqué, donc n'importe où). La règle doit viser transfers-menu lui-même. Le bouton, lui, porte déjà un [hidden] natif quand la liste est vide.

Points à arbitrer (recommandation en italique, aucun ne bloque le démarrage) : persistance de l'historique — en mémoire, vidé à la fermeture de Tabby, plutôt que localStorage ou config.yaml (qui n'a pas à grossir d'un journal) ; périmètre — nos transferts uniquement, ou refléter aussi ceux du panneau SFTP natif et des autres plugins en s'abonnant à fileTransferStarted$ (recommandation revue après la question du 2026-07-29 sur le menu natif : tout refléter, cf. option (c) ci-dessus).

Placement en bas de sidebar

Demandé explicitement en bas. À noter pour ne pas le redécouvrir comme une incohérence : le panneau des tunnels actifs avait été sorti du bas de sidebar au profit d'une position sous les sessions actives, l'arbre étant contraint en hauteur. Trois sections repliables empilées (sessions, tunnels, transferts) risquent d'écraser l'arbre — d'où l'importance du « masqué quand vide », qui est le comportement déjà retenu pour les deux autres.

2. Dépôt depuis l'OS directement sur un dossier distant — ✅ Fait (2026-08-02)

Fait — testé et validé par l'utilisateur le 2026-08-02

Branche feat/depot-sur-dossier-distant, détail dans AI-HISTORY. Le plan ci-dessous a été suivi sur l'essentiel — un seul point de gestion du drop, garde-fou de cible, confirmation groupée avant écrasement, récapitulatif après coup — avec trois écarts, tous tranchés en construisant :

Le lien symbolique est résolu au dépôt et non au survol (un readlink par dragover est hors de question) : la ligne s'allume sur isSymlink, et s'il ne mène pas à un dossier le dépôt retombe sur le dossier affiché et le dit. Quand il y mène, la destination est le chemin du lien, que le serveur résout — le lien n'est jamais touché. Le retour visuel remplace le voile plein cadre de la directive : ligne ciblée allumée, ou à défaut cadre du corps, jamais les deux.

Aujourd'hui le dépôt fonctionne mais atterrit toujours dans le dossier affiché : la template pose dropZone (la directive de tabby-core) sur .sftp-body, qui émet un DirectoryUpload transmis à uploadOneFolder(), lequel écrit sous this.path.

Écrasement silencieux d'un fichier distant

Point établi, pas une hypothèse : SFTPSession.upload() écrit dans <path>.tabby-upload puis renomme par-dessus la cible (documenté dans src/sftpLocalTransfer.ts). Un dépôt sur le mauvais dossier écrase donc sans un mot un fichier de même nom déjà présent sur le serveur. Le geste devenant beaucoup plus facile à déclencher par accident une fois qu'on peut viser n'importe quelle ligne, prévoir un garde-fou : stat() préalable sur chaque cible et confirmation groupée en cas de collision (le panel-conflict-resolver de tabby-sftp-plus reste la référence citée pour ce sujet), plus un récapitulatif après coup « N fichiers envoyés vers <chemin> ».

3. Glisser-déposer interne au panneau SFTP — ✅ Fait (2026-08-02)

Fait — validé dans l'usage par l'utilisateur le 2026-08-02

Branche feat/deplacement-interne-sftp. Le plan ci-dessous a été suivi — drag HTML5, distinction par dataTransfer.types, rename(), garde-fous — avec un ajout qui n'était pas prévu et qui a demandé un arbitrage, plus deux points tranchés en construisant.

Le geste de départ était déjà pris. Le glisser-déposer sortant occupe le même dragstart, et les deux ne se partagent pas de la même façon selon la nature de l'entrée :

Tranché avec l'utilisateur le 2026-08-02 : les deux, en deux temps. Glisser un dossier le déplace, toujours. Le sortir de la fenêtre et le lâcher là déclare l'intention : la copie démarre et le geste suivant sur cette même ligne l'emporte vers l'OS. Ce n'est pas un détour inventé pour l'occasion — c'est la forme que la copie d'un dossier avait déjà (« préparation…, glissez-le à nouveau »), startDrag() exigeant un fichier existant. La sortie de fenêtre se lit à l'âge du dernier dragover reçu par le document, pas à un comptage dragenter/dragleave que Chromium rapporte déjà mal ici. Deux options écartées : donner la priorité à la copie quand le réglage est coché (l'option devenait un choix exclusif et muet), et réserver le geste au seul déplacement en renvoyant la copie au menu contextuel — qui l'offre pourtant bien, sous « Download directory ».

Collision : refus, pas d'écrasement (confirmé par l'utilisateur). Même réponse que renameEntry(), et pour la même raison : SFTP v3 laisse au serveur le soin de décider ce que fait un rename sur un nom occupé, et certains remplacent. Écraser demanderait un unlink/rmdir récursif préalable — trop destructif pour un glisser. La lecture passe par readRemoteEntry(), jamais par le stat() que ce plan recommandait plus bas (#50).

La source du geste est retenue par le composant, et pas seulement posée sur le DataTransfer : getData() rend une chaîne vide pendant dragover, où seuls les types sont lisibles — or c'est là que se décide le surlignage. Le chemin annoncé sert au drop, à confirmer que le geste vient bien de ce panneau. Un déplacement impossible n'est pas refusé par un message après coup : preventDefault() n'est pas appelé, donc le dépôt ne peut pas avoir lieu et le curseur le dit.

Franchissement de points de montage : rien de spécial n'a été tenté. SFTP n'offre aucun moyen de savoir où un chemin est monté ; l'échec du serveur est remonté tel quel.

Deux retours de la passe manuelle, à traiter

Validé dans l'usage, avec deux réserves que l'utilisateur a explicitement renvoyées à plus tard :

Sans objet depuis le 2026-08-03 : les deux tiennent au geste en deux temps, que le marqueur de dépôt a supprimé. Un dossier part au premier geste, donc il n'y a plus de notice à faire tenir, et l'offre étant annoncée d'emblée la cible n'affiche plus de refus. Le chemin en deux temps ne subsiste qu'en repli, si le serveur local ne démarre pas — les deux réserves ne valent que pour lui, et ne seront rouvertes que s'il redevient le chemin ordinaire.

Instruit le 2026-08-03 — trois plugins lus, deux conclusions de cette roadmap invalidées

Demandé par l'utilisateur le 2026-08-02 : vérifier ce que les plugins SFTP graphiques existants gèrent déjà de ces mécaniques — déplacement interne, dépôt sur une ligne de dossier, glisser sortant. Étudiés : tabby-sftp-plus 2.0.0 (déjà installé), tabby-sftp-panel 0.3.0 et tabby-sftp-ui 0.2.7 (récupérés depuis npm, hors du dépôt) ; tabby-sftp-ui-next est le même code republié. Les trois publient leur index.js.map avec sourcesContent : leurs sources TypeScript d'origine se lisent en entier, commentaires compris — méthode à préférer à la lecture de bundle compilé employée jusqu'ici. Tous sous licence MIT ; rien n'a été recopié.

Hors périmètre de ce chantier : le glisser-déposer sortant (tirer un fichier distant vers l'Explorateur Windows pour le télécharger). Ce n'est pas un oubli — c'est un mécanisme différent (DownloadURL/fichier promis), sans rapport avec la plomberie ci-dessus. Repris et détaillé dans Glisser-déposer sortant ci-dessous.

Livré le 2026-08-08 · Journal : « Glisser-déposer sortant réel, gestionnaire de transferts et réglages (2026-08-01 → 2026-08-02) » (2026-08-02), « Dépôt depuis l'OS sur un dossier distant visé (2026-08-02) » (2026-08-02), « Déplacer une entrée distante au glisser-déposer (2026-08-02) » (2026-08-02), « Étude des plugins SFTP existants (2026-08-03) » (2026-08-03), « G5 — transferts fiables et tunnels à état de reprise (2026-08-07) » (2026-08-07), « G2, G3 et G4 — la phase 0.3.0 close, une passe de test presque tout entière pilotée, et un socle d'i18n (2026-08-08) » (2026-08-08) · Pièges issus du chantier : #56, #64, #65, #66 · Commits : 4a749f1, ae9f3de, abb5cfe, 6d0bad2, 26c152c, 45a8ecf, 0114245, 4e25209, ccb586b, d284e62 (commits du lot de livraison, partagés entre ses chantiers)

Panneau SFTP : suivre l'état réel de la session ✅ Fait (2026-08-03)

Ouvert le 2026-08-02 sur un bug remonté en test, élargi par ce que le diagnostic a mis au jour. Les quatre premiers points partagent la même mécanique — savoir si la session porte encore un SFTP utilisable — et se sont traités ensemble. Options tranchées avec l'utilisateur le 2026-08-02. Points 1 et 2 livrés, testés et validés le 2026-08-02 (branche fix/sftp-session-perdue, 4 commits, détail dans AI-HISTORY). Point 3 sans objet — sa prémisse était fausse, et ce qui a été livré à la place l'a été sur fix/transferts-interrompus. Point 4 livré, testé et validé le 2026-08-03 (branche fix/etat-transitoire). Trois constats en sont sortis sans être des manques de ce chantier — bascule sur exit non exercée, downloadFolder resté sur la voie héritée, contrôle d'arrivée pour les téléchargements du menu contextuel — repris dans Reste ouvert. Point 5, un nouveau déclencheur de retour automatique, ajouté et livré le 2026-08-08 — le chantier reste clos.

1. Bug : après une perte de connexion, le panneau reste mort jusqu'au kill de l'onglet

Deux causes cumulées, l'une chez nous, l'autre chez Tabby :

Fait — testé et validé par l'utilisateur le 2026-08-02

Une troisième cause manquait, et elle annulait les deux autres. Le premier correctif (477b7e5) reconstruisait bien le panneau sur changement d'identité de session, mais sync() sortait avant de l'appeler, sur tab === this.boundTab : un onglet reconnecté est le même objet, seul tab.sshSession change. Et le garde-fou censé forcer un passage par le placeholder testait sshSession?.open, drapeau qui n'est jamais remis à false (piège #59). Le panneau restait donc mort exactement comme avant le correctif.

L'inconnue est levée, et par lecture du code plutôt qu'au test : Tabby fabrique bien un nouvel objet. SSHSession.destroy() émet willDestroy$, sur quoi le multiplexeur retire la session de sa map, donc setupOneSession() ne retrouve rien à réutiliser. La comparaison d'identité suffit — SFTPSession.closed$ n'a pas été nécessaire.

Le test de vivacité est désormais unique : isLiveSSHTab() dans src/tabs.ts, appelé par le panneau et par « Sessions actives », qui avaient divergé. Le dossier courant est retenu par onglet et réinjecté dans le panneau reconstruit, le cache ne pouvant plus l'assurer sur ce chemin.

La reproduction consignée ici était fausse

sudo systemctl restart sshd ne coupe pas les sessions établies : les sshd enfants survivent au redémarrage du démon parent. Les deux gestes qui reproduisent réellement sont le bouton Reconnect de la barre d'outils de l'onglet (reconnexion rapide, absorbée par le délai de grâce) et sudo ss -K state established '( sport = :22 )' (perte durable, jusqu'à la touche de reconnexion). Symptôme à reconnaître quand le panneau est mort : « Session closed » remonté par russh à chaque geste, dans un panneau qui s'affiche pourtant normalement.

2. Rendre la main aux Profils quand il n'y a pas de SFTP

Tranché : sur échec d'openSFTP() ET sur fin du shell (exit). Le second cas est le plus important et n'était pas dans la demande initiale : après un exit, le transport survit (piège #37) donc le SFTP continue de fonctionner, alors que « Sessions actives » a déjà retiré la ligne — deux blocs de la même sidebar donnant deux réponses opposées à « cette session existe-t-elle ? ». C'est notre incohérence, pas celle de Tabby, et c'est elle qu'on corrige.

Écarté : basculer dès qu'aucun onglet SSH n'est focalisé — une session qui met deux secondes à monter renverrait sur Profils, avec un retour manuel à faire.

Pas de retour automatique quand la session redevient exploitable : l'utilisateur est peut-être reparti travailler dans les profils, et un clic suffit. Réglable, le défaut suivant le comportement retenu ici.

Fait — testé et validé par l'utilisateur le 2026-08-02

Un délai de grâce de trois secondes, non prévu au design, a été ajouté après mesure : sans lui, l'issue dépendait de l'instant du tick d'une seconde — le bouton Reconnect remonte une session plus vite que ça, donc le même geste basculait ou ne basculait pas, au hasard. Une reconnexion qui rentre dans la fenêtre est traitée comme une reconstruction : la vue ne bouge pas et le panneau revient sur son dossier. C'est la généralisation du motif qui avait fait écarter « basculer dès qu'aucun onglet SSH n'est focalisé », juste au-dessus. Contrepartie assumée : pendant ces trois secondes le panneau reste affiché mais inerte.

La bascule s'annonce par une notice (demande de l'utilisateur) : la vue change sous ses yeux sans qu'il ait rien demandé. Le panneau mort est détruit plutôt que mis en cache — il tient un canal SFTP sur un transport disparu.

La bascule sur exit n'a pas pu être observée

Le behaviorOnSessionEnd de l'utilisateur ferme l'onglet à la fin du shell : l'onglet part avec son panneau, et pruneClosedTabs() fait le ménage avant que la bascule ait à se déclencher. Le code la couvre — isLiveSSHTab() lit le shell, pas le transport — mais ce chemin reste non exercé tant qu'un profil n'est pas réglé sur « garder l'onglet ouvert ». L'incohérence avec « Sessions actives » décrite ci-dessus n'est pas davantage observable dans cette configuration : à vérifier le jour où un profil change de réglage, plutôt qu'à supposer acquis.

3. Transfert en cours au moment de la bascule

Retirer le panneau détruit le composant, ce qui libère le canal SFTP et annule le transfert — brutal sur plusieurs Go entamés. Tranché : demander confirmation (la recommandation était d'attendre la fin en silence ; l'utilisateur préfère être consulté). Réutiliser ConfirmModalComponent, jamais un dialogue OS (invariant).

Sans objet — la prémisse ci-dessus est fausse (vérifiée le 2026-08-02)

Retirer le panneau ne libère pas le canal et n'annule pas le transfert. ngOnDestroy ne touche pas this.sftp ; SFTPSession n'expose aucun close(), le canal ne meurt qu'avec la session SSH ; sftp.download() est une promesse portée par tabby-ssh et le registre des transferts vit dans root — aucun des deux ne dépend du composant. De plus, la seule bascule automatique est celle d'une session perdue, où le transfert est déjà condamné : la confirmation s'afficherait après la mort de ce qu'elle prétend protéger.

Ce que la vérification a trouvé à la place, et qui est le besoin réel derrière la demande : un transfert tué par la perte du transport restait affiché « en cours » à vie. Livré, testé et validé le 2026-08-02 (branche fix/transferts-interrompus, 4 commits, détail dans AI-HISTORY) : état failed et ligne « interrompu à N % », dispose() qui n'efface plus un temporaire en cours d'écriture, téléchargement du menu contextuel servi par notre offre HTTP pour que son échec soit visible, et arrêt réel du transfert quand le client s'en va.

Un écart connu à ne pas retraiter comme un manque : la voie Chromium transfère pendant que la boîte « Enregistrer sous » est ouverte (les octets d'une annulation sont perdus, rien ne s'écrit chez l'utilisateur) — comportement constaté en test et assumé, l'alternative ayant été proposée puis écartée. Le second écart de cette liste est levé : downloadFolder est passé sur la voie du plugin le 2026-08-07 (fiche mise à jour dans « Reste ouvert »), et « Download directory » dit désormais « interrompu à N % » sur une coupure, comme le reste.

4. Ne plus annoncer « terminé » pour un glisser sortant

Mesuré : 22 s d'écart entre notre « terminé » et l'écriture réelle du fichier (piège #58). Tranché : un état transitoire pendant la copie, l'utilisateur acceptant explicitement qu'il soit approximatif (« mentir au pire c'est pas très grave ici »).

Le contrôle d'existence demandé n'est pas possible sur ce chemin

Demande de l'utilisateur : vérifier que le fichier est bien arrivé là où il a été déposé, « comme d'autres plugins SFTP le font ». Impossible pour un glisser-déposer — Windows ne communique jamais la destination à l'application source, et will-download d'Electron ne rendrait que le chemin temporaire de Chromium. Les plugins qui affichent ce contrôle le font sur des téléchargements avec sélecteur de chemin, où ils ont eux-mêmes choisi la destination. Ce contrôle est en revanche faisable pour nos téléchargements par le menu contextuel, qui imposent leur chemin à platform.startDownload() — à traiter comme un point distinct si le besoin est confirmé.

Reste donc à trancher au moment de coder : sur quoi lever l'état transitoire, puisque rien ne signale la fin de la copie du shell. Piste la moins mauvaise : une durée estimée d'après la taille et le débit observé, assumée comme approximative.

Rouvert le 2026-08-03 : l'impossibilité énoncée ci-dessus ne vaut que pour l'API

L'étude des plugins existants a trouvé un contournement chez tabby-sftp-panel — voir marqueur de dépôt. La destination reste effectivement hors de portée de toute API, mais elle se retrouve en faisant écrire par le shell un marqueur reconnaissable, puis en le cherchant dans les dossiers plausibles. Deux conséquences pour ce point 4 : la fin du dépôt cesse d'être à estimer, puisque la requête HTTP du marqueur la signale ; et le contrôle « le fichier est-il bien arrivé ? », déclaré ici impossible sur ce chemin, redevient faisable — c'est même le plugin qui écrit le fichier, donc le contrôle est entier. Ce point ne se code donc pas avant que le marqueur soit tranché : le faire d'abord reviendrait à construire l'approximation que le marqueur rend inutile.

Réduit au fichier seul depuis le 2026-08-03. Le marqueur de dépôt est livré, mais il ne sert que le dossier : un fichier reste annoncé en DownloadURL portant ses vrais octets, donc c'est encore le shell qui l'écrit et l'écart mesuré de 22 s subsiste pour lui. Deux issues, à arbitrer : passer aussi les fichiers par le marqueur — une seule voie, fin réelle et destination connue, au prix d'une heuristique sur un cas qui marche aujourd'hui sans elle — ou garder la voie directe et se contenter d'un état transitoire approximatif, que l'utilisateur avait déjà accepté comme tel.

Fait — testé et validé par l'utilisateur le 2026-08-03

Branche fix/etat-transitoire, détail dans AI-HISTORY. L'unification a été écartée, et pas seulement par prudence : le marqueur suppose que la destination est un dossier du système de fichiers. C'est vrai d'un dossier glissé, dont aucune autre cible ne voudrait ; ce ne l'est pas d'un fichier, dont les cibles utiles — champ d'envoi d'un navigateur, client de messagerie — ne sont pas des dossiers. Elles recevraient le marqueur vide pendant que le fichier partirait dans le dossier de repli.

Retenu à la place : un état handover entre la fin de notre flux et la remise réelle, levé sur une durée estimée d'après la taille — 200 Mo/s supposés là où 250 ont été mesurés, donc la ligne finit tard plutôt que tôt, ce qui est le sens d'erreur voulu. Plancher de 400 ms pour qu'un petit fichier montre l'état sans clignoter, plafond de deux minutes pour qu'une mauvaise estimation ne fige jamais une ligne. L'infobulle dit que la durée est estimée.

Un état terminal peut avoir encore besoin du minuteur. « Ce qui peut être annulé » et « ce qui a besoin du tick » ont dû être séparés : une ligne en remise n'a plus rien d'annulable chez nous, mais son échéance n'est surveillée par rien d'autre — l'arrêt du minuteur au dernier transfert actif la laissait figée sur « remise au système » pour le reste de la session. Conséquence visible et vérifiée au test : la vider ne demande pas confirmation et ne supprime pas le fichier déposé.

5. Retour automatique sur Profils quand plus aucun onglet n'a de SFTP actif ✅ Fait (2026-08-08)

Ajouté pendant G4, à ne pas confondre avec le délai de grâce de trois secondes du point 2 ci-dessus : celui-ci réagit à la mort de la session de l'onglet lié. Le nouveau réglage couvre l'autre déclencheur — plus aucun onglet SSH avec SFTP actif nulle part, quel que soit l'onglet qui a ouvert la vue. Réglage sftpAutoReturnToProfiles, activé par défaut (tranché par l'utilisateur le 2026-08-07, contrairement au « pas de retour automatique » retenu au point 2 — les deux décisions ne se contredisent pas, elles répondent à deux questions différentes). Réutilise isLiveSSHTab() (src/tabs.ts), jamais sshSession.open (piège #59) — même sonde unique de vivacité que le reste de ce chantier.

Livré le 2026-08-08 · Journal : « Panneau SFTP : suivre l'état réel de la session (2026-08-02) » (2026-08-02), « Transferts : montrer qu'un transfert est mort, et le servir nous-mêmes (2026-08-02) » (2026-08-02), « Plus de « terminé » avant que le système ait fini (2026-08-03) » (2026-08-03), « G2, G3 et G4 — la phase 0.3.0 close, une passe de test presque tout entière pilotée, et un socle d'i18n (2026-08-08) » (2026-08-08) · Pièges issus du chantier : #57, #58, #59, #60, #61 · Commits : 477b7e5, 2aa610b, 11d3d54, 697a624, ca6dde1, 8cff07d, e73e578, a366696, 1ded769, d284e62 (commits du lot de livraison, partagés entre ses chantiers)

Historique des Profils Récents ✅ Fait (2026-08-08)

Liste dynamique et pliable affichant les 5 derniers profils de connexion utilisés, positionnée juste en dessous de la zone des sessions actives pour un réengagement ultra-rapide sans navigation dans l'arborescence.

Fait, désactivé par défaut

Livrée en MRU : jusqu'à MAX_RECENT_PROFILES = 5 entrées, plus récent en tête, dédoublonnées, dans localStorage.sidebarPlusRecentProfiles — délibérément pas dans config.yaml, un historique de connexion étant local à la machine, pas à synchroniser. Réglage sidebarPlus.showRecentProfiles, désactivé par défaut — décision de l'utilisateur, la présentation actuelle de la section étant jugée à améliorer avant de l'imposer par défaut. Reste à reprendre dans un lot ultérieur.

Livré le 2026-08-08 · Journal : « G2, G3 et G4 — la phase 0.3.0 close, une passe de test presque tout entière pilotée, et un socle d'i18n (2026-08-08) » (2026-08-08) · Pièges issus du chantier : — · Commits : d284e62 (commits du lot de livraison, partagés entre ses chantiers)

Tunnels SSH

Conçu comme une surcouche visuelle branchée directement sur le moteur de port-forwarding déjà natif de Tabby — pas de réimplémentation du tunneling lui-même. Le plugin se contente d'exposer la création/config en un clic depuis le menu contextuel d'un profil (plutôt que via la fenêtre de gestion des tunnels native, jugée plus lourde et déconnectée des profils), et de refléter l'état des tunnels déjà gérés par Tabby.

Fait — testé et validé par l'utilisateur le 2026-07-29 sur de vrais tunnels

Vérification d'API préalable sur le paquet installé (typings + bloc d'export runtime, méthode du piège #13/#32). Livré :

Périmètre restreint par choix utilisateur — ne pas le rouvrir comme un manque

Une entrée « Gérer les tunnels… » déléguant à la modale native de Tabby (SSHTabComponent.showPortForwarding()) a été construite, testée et validée, puis retirée à la demande explicite de l'utilisateur (« je veux uniquement ma solution maison »).

Conséquence actée : rien ne peut plus agir sur une session déjà ouverte. Tout passe par la configuration du profil, lue par Tabby au seul démarrage d'une session. Ce n'est pas un oubli et ce n'est pas contournable : SSHSession.addPortForward() appelle fw.startLocalListener(), donc exige une instance de ForwardedPort — classe absente du bloc d'export runtime de tabby-ssh. La reconstruire depuis le prototype d'un tunnel existant ne marcherait que si l'utilisateur en avait déjà un, ce qui est circulaire.

D'où deux garde-fous : un toast après enregistrement rappelant que le tunnel montera au prochain lancement, et la suppression bloquée (cadenas + infobulle) sur les seuls tunnels effectivement montés — retirer leur configuration les laisserait tourner pendant que l'utilisateur les croit supprimés. Un tunnel configuré mais non monté se supprime librement.

Écarts au design ci-dessus : le panneau est sous les sessions actives et non en bas de sidebar (l'arbre y est contraint en hauteur, et les deux blocs d'état live se lisent mieux ensemble) ; le bouton navigateur n'apparaît que sur les tunnels Local (un Remote écoute à l'autre bout, un Dynamic est un proxy SOCKS), et https n'est déduit que des ports usuels.

Non traité : l'affichage de l'état de reprise sur micro-coupure réseau.

État de reprise et dédoublonnage — fait, passe pilotée + visuelle 24/24 le 2026-08-07

Le point de départ est un fait établi sur les sources installées : il n'existe aucune reprise native à refléter. Une micro-coupure détruit la SSHSession entière ; seuls les tunnels écrits dans le profil sont remontés quand l'onglet se reconnecte (start() rejoue profile.options.forwardedPorts, rien d'autre), et un tunnel ajouté à la volée par la modale disparaît en silence. L'état de reprise est donc porté par le plugin : mémoire des tunnels par onglet (l'objet onglet survit à la reconnexion, seul sshSession change), clé de corrélation tunnelKey().

Trois états sur les lignes : waiting — la session est morte, la ligne reste, grisée « reprise… », pendant 60 s (une section qui disparaît pendant la coupure est indiscernable d'une section qui n'a jamais eu de tunnel) ; retour à live si la reprise remonte le tunnel ; lost — orange, « non remonté », 30 s après une reconnexion qui n'a pas ramené un tunnel mémorisé : c'est le témoin de la perte silencieuse ci-dessus.

Les tunnels affichés en 3-4 exemplaires (demande utilisateur du 2026-08-07) venaient du multiplexage (reuseSession) : plusieurs onglets partagent la même session, et le panneau listait le même tableau une fois par onglet. Dédoublonnage par identité d'objet du ForwardedPort. Pour deux sessions distinctes, le second bind local est refusé par l'OS sur ce poste (EADDRINUSE mesuré, y compris à chaud) : le démontage automatique de doublon (premier monté gagne, notice) est en place mais son chemin est non exercé et non atteignable ici — ne pas le présenter comme validé. Comportement vérifié au test 20, voulu : fermer un onglet d'une session multiplexée laisse le tunnel vivant tant qu'un autre onglet la référence (comptage par référence côté Tabby).

Livré le 2026-08-07 · Journal : « Tunnels SSH (2026-07-29) » (2026-07-29), « G5 — transferts fiables et tunnels à état de reprise (2026-08-07) » (2026-08-07) · Pièges issus du chantier : — · Commits : c4422f6, ccb586b (commits du lot de livraison, partagés entre ses chantiers)

Glisser-déposer sortant (téléchargement vers l'OS)

Demandé par l'utilisateur le 2026-07-30 — le geste symétrique du dépôt entrant du chantier Transferts SFTP : glisser un fichier ou dossier distant hors de la sidebar et le lâcher dans l'Explorateur Windows (ou toute autre cible OS) pour le télécharger à cet endroit. Explicitement hors périmètre du chantier Transferts SFTP (mécanisme différent), repris ici comme chantier séparé.

Contrainte explicite de l'utilisateur : ne jamais faire quitter/planter Tabby

Le geste doit être totalement transparent pour l'application — pas de fermeture, pas de gel, pas de plantage pendant ou après le drag, quelle que soit la cible du dépôt (y compris un dépôt annulé ou hors d'une cible valide).

Fait — testé et validé par l'utilisateur le 2026-08-01

Écrit le 2026-07-30 (8a89787), testé le 2026-08-01 seulement : le commit était resté sans passe de test et a été poussé entre-temps par la campagne de gouvernance, donc du code non exécuté a séjourné sur master public. C'est exactement ce que l'option branches du PROFIL.md évite depuis : un chantier non abouti vit sur une branche.

Livré : glisser un fichier ou un dossier distant vers l'OS. Conception en deux temps imposée par piège #49 — le premier geste télécharge une copie temporaire et n'aboutit généralement nulle part, le second part instantanément. Dossiers derrière une option décochée par défaut, avec confirmation au-delà de 25 fichiers ou 20 Mo. Un lien symbolique vers un dossier est ignoré plutôt que téléchargé (piège #45), faute de quoi une seule entrée de ce type faisait échouer tout le dossier. Copies supprimées à la destruction du panneau, jamais sur dragend : Windows lit le fichier au moment du dépôt, parfois après la fin du geste.

Les deux points signalés comme non vérifiés se sont révélés sains : relâcher au dessus du panneau SFTP lui-même ne déclenche pas son dropZone (pas de ré-upload de la copie, pas d'écrasement), et draggable='true' sur la ligne n'a pas mangé le double-clic. Aucun garde-fou supplémentaire n'a donc été nécessaire.

Trois suites, portées par le chantier ci-dessous : l'absence de tout retour de fin de préparation (sur un dossier lent, un geste qui ne produit rien est indiscernable d'une panne), la fraîcheur de la copie resservie, et la lenteur du téléchargement d'un dossier.

Dépassé le 2026-08-02 : le transfert démarre désormais au dépôt

Tout ce qui précède décrit la conception en deux temps, imposée par piège #49. Elle n'est plus le chemin principal depuis le 2026-08-02 : Chromium offre une autre route que startDrag() — une entrée DownloadURL dans le DataTransfer du glisser, qui annonce un nom et une URL dont le contenu n'est réclamé qu'au moment du dépôt. C'est le rendu différé de Windows, atteint par le moteur plutôt que par une API Electron. Le plugin sert lui-même cette URL depuis un serveur HTTP sur la boucle locale, port éphémère, un jeton aléatoire à usage unique par fichier, rien d'énumérable (src/dragOutServer.service.ts).

Conséquence pratique : un fichier part en un seul geste, sans copie préalable, et le transfert apparaît dans le gestionnaire au moment où les octets bougent. Le chemin en deux temps décrit plus haut subsiste comme repli — il sert encore aux dossiers (une offre DownloadURL ne transporte qu'un fichier) et à toute installation où le serveur local ne démarre pas.

Marqueur de dépôt : un dossier en un seul geste, et la destination connue ✅ Fait (2026-08-03)

Issu de l'étude des plugins existants : tabby-sftp-panel contourne ce que le piège #58 donnait pour indépassable. Le principe tient en une inversion — puisque l'offre DownloadURL ne transporte qu'un fichier et que le shell ne dit jamais où il l'écrit, n'annoncer pas la charge utile mais un marqueur de 0 octet au nom unique, laisser le shell l'écrire dans le dossier visé, puis l'y retrouver pour apprendre cette destination et y livrer soi-même le contenu par SFTP.

Fait — testé et validé par l'utilisateur le 2026-08-03

Branche feat/marqueur-depot, détail dans AI-HISTORY. Cinq cas validés au geste réel : dépôt dans une fenêtre de l'Explorateur, sur le Bureau, sur l'icône d'un dossier (le niveau de sous-dossiers que le balayage couvre), déplacement interne d'un dossier inchangé, et glisser d'un fichier inchangé. La cohabitation était l'enjeu — le glisser reste un glisser HTML5 ordinaire, donc le déplacement interne et la sortie vers l'OS ne se disputent plus le même geste.

Ce que le test a établi et que le design n'avait pas prévu : une cible qui ne comprend pas l'offre — Terminal Windows, MobaXterm — ne réclame jamais l'URL, donc rien ne se produit, pas même le repli (piège #67). C'est le bon comportement : le repli ne doit couvrir que la cible qui a accepté sans qu'on retrouve où. Écart assumé par l'utilisateur, aucun avertissement n'étant possible depuis la source.

Non exercé : le repli lui-même — livraison dans ~/Downloads quand le marqueur n'est pas retrouvé. Le code est en place, aucun geste ne l'a parcouru, et le cas ci-dessus n'est pas celui-là. Il s'exercerait en déposant sur un nœud du volet de navigation pointant à deux niveaux sous une fenêtre ouverte.

Sans garde-fou de poids, contrairement à l'ancien chemin : le téléchargement démarre au dépôt, sans question sur un dossier volumineux. Retenu tel quel — le transfert est désormais visible dans la liste de Tabby et annulable, ce que la préparation silencieuse d'avant n'offrait pas.

Livré le 2026-08-03 · Journal : « Passe de test du glisser-déposer sortant (2026-08-01) » (2026-08-01), « Glisser-déposer sortant réel, gestionnaire de transferts et réglages (2026-08-01 → 2026-08-02) » (2026-08-02), « Étude des plugins SFTP existants (2026-08-03) » (2026-08-03), « Marqueur de dépôt : un dossier sort en un seul geste (2026-08-03) » (2026-08-03) · Pièges issus du chantier : #49, #55, #67 · Commits : 8a89787, 06262b5, 03cab1a

Intégrité des données au re-parentage et à la suppression ✅ Fait (2026-08-02)

Livré et testé le 2026-08-02

Branche fix/integrite-donnees, commit 3128e55. Passe de test pilotée dans Tabby sur des entrées grp-zzz-test-*, détaillée dans AI-HISTORY : les quatre scénarios passent, la non-régression aussi, et la configuration réelle a été retrouvée identique champ à champ après coup.

Le scénario des « Épinglés » a été rejoué avec l'ancien comportement pour vérifier que le test prouve quelque chose : il reclassait bien deux profils réels de l'utilisateur. Ce n'était donc pas un défaut théorique.

Ce reste a été traité le 2026-08-03 (3884789) : les ids morts après une suppression sont retirés au point de suppression, par forgetDeletedId(). Détail dans la fiche des trois défauts mineurs ci-dessous, avec la raison pour laquelle ce n'est pas pruneDeadOrderIds() qui a été élargi.

Trois défauts trouvés à la revue de code du 2026-08-02, tous dans le même domaine : les gestes de la sidebar qui réécrivent config.store.groups / .profiles — celui-là même qui a déjà corrompu la configuration de production une fois (piège #12). Aucun n'a été corrigé à chaud : chacun demande une passe de test sur des entrées grp-zzz-test-*, comme l'impose l'option discipline-test du PROFIL.

1. Le re-parentage d'un dossier perd ses defaults de groupe

reparentGroup() (sidebarTree.component.ts) recrée le dossier sous un id neuf en ne recopiant que name, icon, color et parentGroupId, puis supprime l'ancien. Or l'interface ProfileGroup de tabby-core porte aussi un champ defaults, que Tabby lit à l'exécution par getProviderProfileGroupDefaults() et fusionne dans le ConfigProxy de chaque profil du dossier. Ces réglages hérités disparaissent donc du config.yaml, sans erreur ni avertissement — et récursivement, la fonction se rappelant sur chaque sous-dossier.

Ce que ça donne à l'usage : un dossier « Prod » portant des defaults SSH de groupe (utilisateur, port, clé, jump host) glissé dans un autre dossier depuis la sidebar, et tous ses profils perdent leur héritage.

Correctif envisagé : recopier defaults — et, tant qu'à faire, n'importe quel champ que la version installée de Tabby ajouterait — plutôt que d'énumérer les quatre champs connus. Reste à décider si l'on recopie le groupe entier moins l'id, ou si l'on maintient une liste explicite : la première option est robuste aux évolutions de tabby-core, la seconde dit ce qu'elle fait.

2. Sortir un profil des « Épinglés » réordonne des profils d'autres dossiers

« Épinglés » est un groupe synthétique : ses entrées vivent réellement dans des dossiers différents. On ne peut rien y déposer, mais on peut parfaitement en sortir un profil vers un vrai dossier — CDK résout la cible d'un glisser parmi les listes connectées à la source. Le dépôt appelle alors persistProfileOrder('favorites', …) avec ce qui reste affiché dans « Épinglés ». Sur le workspace « Tous », cela descend jusqu'à persistProfileWeights(), qui écrit weight = index sur chacun de ces profils — selon leur rang dans la liste des favoris, pas dans leur dossier.

Ce que ça donne à l'usage : cinq profils épinglés répartis dans trois dossiers, on en glisse un vers un dossier, et les quatre autres remontent ou descendent dans leurs dossiers respectifs au prochain rechargement de l'arbre. Sur un workspace nommé, l'effet est bénin — la clé morte profileOrder['favorites'] est nettoyée à l'écriture suivante. Le glisser d'une multi-sélection est épargné, lui : il passe par moveSelectionToGroup(), qui n'itère que de vrais groupes.

Correctif envisagé : ne persister l'ordre de la source que lorsqu'elle est un vrai dossier. Les deux conteneurs synthétiques (favorites, search) sont identifiables par leur id.

3. Un dossier « vide » peut ne l'être que dans le workspace regardé

deleteGroup() refuse de supprimer un dossier non vide en comptant group.children et group.profiles — c'est-à-dire le nœud de l'arbre affiché, donc filtré par le workspace actif. Un dossier dont tout le contenu est masqué dans ce workspace paraît vide et passe le garde-fou. deleteProfileGroup() supprime alors le groupe et retire son group à chaque profil qui y vivait : les profils masqués retombent dans « Ungrouped », visibles partout, et les sous-dossiers masqués remontent à la racine.

Correctif envisagé : compter sur rawGroupsSnapshot, le cliché non filtré que le composant tient déjà et qu'isSelfOrDescendant() et rescueTargetGroupId() consultent précisément pour ce genre de garde. Le message d'erreur gagnerait à distinguer les deux cas — « contient 2 profils » et « contient 2 profils, masqués dans ce workspace » ne demandent pas la même action.

Trois défauts mineurs du même voisinage — tous corrigés

Fiche rectifiée le 2026-08-03

Elle décrivait encore les trois comme ouverts alors que le code en traite deux depuis le 2026-08-02 : ils ont été corrigés en construisant le chantier ci-dessus, sans que cette fiche-là, qui n'était pas la sienne, en reçoive la trace. C'est l'un des deux cas qui ont fait passer l'option seuil à roadmap-libre le 2026-08-03 — une roadmap périmée ment autant qu'une roadmap écrite sans arbitrage. Vérifié dans src/, pas déduit du journal.

Livré le 2026-08-03 · Journal : « Revue générale, remise en ordre des documents, et intégrité des données (2026-08-02) » (2026-08-02), « Un id supprimé ne laisse plus de trace derrière lui (2026-08-03) » (2026-08-03) · Pièges issus du chantier : #62, #74 · Commits : 3128e55, 3884789

Ouverture groupée d'un dossier ("Group Exec") ✅ Clos (2026-08-03)

Clic droit sur un dossier → "Ouvrir tout" : lance une session pour chaque profil du dossier, en onglets séparés ou en split panes. Intérêt renforcé si couplé à la saisie synchronisée déjà native de Tabby (multi-input) pour exécuter la même commande sur plusieurs serveurs en parallèle.

Clos sans écrire une ligne — 2026-08-03

Ce n'était pas un item neuf. « Lancer toutes les sessions » existait déjà (launchGroupSessions()) ; seuls les trois enrichissements ci-dessus restaient, et l'utilisateur les a refusés, chacun pour un motif tenant à l'usage réel :

Ce qui existait devient donc l'état final, et le commentaire de launchGroupSessions() a été réécrit dans le même geste : il annonçait une « version minimale » et renvoyait le reste à cette fiche. Le laisser aurait fait lire un reste à faire là où il n'y en a plus — la règle de détection de dérive appliquée dans le sens où c'est le code, et non la doc, qui portait l'hypothèse périmée.

Livré le 2026-08-03 · Journal : « G1 — duplication, snippets, notes et partage de groupe (2026-08-03) » (2026-08-03) · Pièges issus du chantier : — · Commits : e2cea77 (commits du lot de livraison, partagés entre ses chantiers)

Clone Express de profil ✅ Fait (2026-08-03)

Clic droit sur un profil → action "Dupliquer le profil". Génère instantanément une copie parfaite du profil sous le même groupe (avec suffixe " - Copie").

Fait — testé et validé par l'utilisateur le 2026-08-03

Commit e2cea77. structuredClone() avant newProfile(), comme la fiche l'exigeait et pour la raison exacte qu'elle donnait : newProfile() pousse dans config.store.profiles l'objet qu'on lui donne, donc une copie de surface laisserait les deux entrées partager un même options — éditer l'une éditerait l'autre.

« X - Copie - Copie » n'est pas un défaut, et a été lu deux fois comme tel pendant la passe de test : c'est la copie qui avait été dupliquée, et ce nom-là est libre. À retenir davantage que le cas : du code fonctionnel avait été supprimé sur la foi du rapport avant d'être rétabli. Un résultat inattendu se confronte au geste exact qui l'a produit avant de conclure que le code se trompe.

Livré le 2026-08-03 · Journal : « G1 — duplication, snippets, notes et partage de groupe (2026-08-03) » (2026-08-03) · Pièges issus du chantier : — · Commits : e2cea77 (commits du lot de livraison, partagés entre ses chantiers)

Quick Snippets (Macros de Sidebar) ✅ Fait (2026-08-03)

Permet d'associer des commandes textuelles récurrentes (ex. redémarrage de service, consultation de logs) à un profil ou un groupe.

La fiche d'origine ne décrit pas ce qui a été construit

Elle attachait des commandes à un profil ou à un dossier, et faisait tout tenir dans un sous-menu du clic droit. Quatre demandes successives de l'utilisateur, pendant l'implémentation, en ont fait autre chose. Le paragraphe ci-dessus est conservé pour mémoire ; ce qui suit fait foi.

Fait — testé et validé par l'utilisateur le 2026-08-03, au terme de cinq passes (46 + 20 + 10 + 10 + 10 points)

Commit e2cea77.

Ce qui a changé de nature, et pourquoi

Où vit le code

src/snippets.service.ts porte tout le modèle — bibliothèque, rattachements, variables, réglages, résolution, expansion, cycle de vie des ids. Les trois vues n'en sont que des façades : la sidebar (qui exécute), snippetsModal.component (qui rattache et renseigne) et l'onglet de réglages (qui écrit les commandes).

Le premier jet gardait tout dans sidebarTree.component.ts, et l'onglet de réglages avait déjà sa propre copie de l'expression régulière des variables, avec un commentaire qui l'assumait. C'est ce signal qui a décidé de l'extraction : le composant perd ~25 000 caractères et la duplication disparaît. Le service ne va pas chercher les groupes lui-même — la sidebar lui passe son instantané non filtré à chaque chargement, getProfileGroups() étant asynchrone alors que l'arbre le tient déjà.

Modèle de données

Quatre clés sous sidebarPlus, déclarées dans les defaults du ConfigProvider (#16) : snippetLibrary (les commandes, une fois chacune), snippetAttachments, snippetVariables, snippetSettings, plus snippetOverrides et snippetMuted venus de la passe de test.

Tout sauf la bibliothèque est rangé par id — donc soumis au #62

Un dossier re-parenté reçoit un nouvel id : migrateWorkspaceGroupId() reporte les maps, forgetDeletedId() les collecte à la suppression. Oubliés, un dossier glissé ailleurs perdrait en silence ses snippets — et tous ceux que ses profils en héritaient. La bibliothèque, elle, n'est touchée par aucun des deux : rien n'y est rangé par id de profil ou de dossier, et c'est précisément ce que gagne la séparation.

Héritage — une seule chaîne, quatre usages

Du plus précis au plus général : le profil → le dossier qui le contient → son parent → la racine. Elle sert aux snippets (on cumule) et aux variables comme aux deux réglages (la première réponse rencontrée gagne). Bornée contre un cycle de parents, et lue sur rawGroupsSnapshot : un dossier masqué dans le workspace courant porte quand même les snippets dont ses profils héritent — un workspace règle ce qui s'affiche, jamais ce qu'un profil est.

Variables

Les réglages, tri-état

Tous répondables au profil comme au dossier — « un dossier de prod et un dossier de sandbox ne doivent pas avoir les mêmes possibilités » — et par snippet et par élément : la résolution demande, à chaque maillon, la réponse propre au snippet avant la réponse générale de l'élément (snippetOverrides).

undefined = « hérite », et l'option le dit à l'écran (« Hériter (écrire) ») pour que le choix ne soit jamais aveugle.

Limites assumées

Deux défauts trouvés en test, à ne pas redécouvrir

Suite ouverte — raccourcis clavier par snippet et boutons au survol

Demandé pendant la passe de test, reporté après stabilisation. Le design tel qu'il a été posé : un raccourci appartient au snippet et vaut partout où il est rattaché (pas un raccourci par rattachement) ; il faut détecter les conflits avec les raccourcis de Tabby comme avec ceux du plugin ; et deux ou trois snippets doivent être atteignables au survol d'une session dans la sidebar, pour partir « à l'aveugle » dans cette session-là.

Point technique à lever avant de s'y mettre : HotkeyProvider ne déclare aujourd'hui que deux entrées fixes (src/hotkeys.ts). Des raccourcis dynamiques, redéclarés à chaque changement de la bibliothèque, n'ont aucun précédent dans ce plugin — c'est là qu'est le risque, pas dans l'interface.

Livré le 2026-08-03 · Journal : « G1 — duplication, snippets, notes et partage de groupe (2026-08-03) » (2026-08-03) · Pièges issus du chantier : — · Commits : e2cea77 (commits du lot de livraison, partagés entre ses chantiers)

Micro-champ de recherche textuelle épinglé en haut de la liste pour filtrer instantanément les profils et groupes affichés sans changer de workspace. Focus via le raccourci Ctrl+F ou Cmd+F lorsque la sidebar est active.

Fait — testé et validé par l'utilisateur le 2026-08-03

Branche feat/filtre-rapide, détail dans AI-HISTORY. Cette fiche décrivait un chantier neuf pour quelque chose qui existait déjà : le champ est en haut de l'arbre depuis le socle, et il cherchait déjà partout, workspace ignoré. Ne manquaient que le raccourci, l'échappement et la portée — vérifié dans le code avant d'écrire quoi que ce soit, et arbitré avec l'utilisateur. À retenir pour les fiches voisines de ce bas de roadmap, écrites tôt : elles décrivent une intention, pas un état.

Livré : Ctrl+F place le curseur et sélectionne le contenu du champ (un second geste écrase donc la recherche précédente au lieu de s'y ajouter) ; Échap vide le filtre et rend le focus à l'arbre ; la recherche couvre options.host et options.user en plus du nom et de la description ; les dossiers dont le nom correspond sont rendus au-dessus des profils trouvés, avec leur contenu et dépliés.

Deux points tranchés en construisant. Le raccourci bascule d'abord sur la vue Profils quand la sidebar montre le SFTP, le champ n'y existant pas — ne rien faire ferait passer le raccourci pour cassé. Et les dossiers, comme les profils, sont cherchés dans l'instantané non filtré par le workspace : un workspace règle ce que l'arbre montre, jamais ce qu'on peut trouver — c'est la décision de 2026-07-28, étendue aux dossiers.

Un bug préexistant fermé au passage

Glisser un profil pendant qu'un filtre est actif persistait l'ordre des résultats comme ordre réel — sur « Tous », dans les weight natifs des profils, sans un mot. Le défaut est antérieur à ce chantier ; l'arrivée des dossiers dans les résultats rendait le geste bien plus facile à déclencher. Le glisser est désormais coupé tant qu'un filtre est actif (piège #68) : ce n'est pas une restriction d'ergonomie à lever un jour, mais la conséquence de ce que l'arbre affiché n'est alors pas l'arbre réel.

Livré le 2026-08-03 · Journal : « Barre de filtrage : raccourci, portée, et un ordre qu'on ne persiste plus (2026-08-03) » (2026-08-03) · Pièges issus du chantier : #68 · Commits : 6de353c

Partage rapide de groupe (Presse-papiers) ✅ Fait (2026-08-03)

Clic droit sur un dossier/groupe → "Copier la structure (JSON)". Extrait l'arborescence complète au format JSON en purgeant obligatoirement tous les secrets, identifiants et mots de passe. L'action symétrique "Coller le groupe" à la racine de la sidebar permet d'importer instantanément la structure.

Fait — testé et validé par l'utilisateur le 2026-08-03, 36/36 sans échec

Commit e2cea77. Le modèle vit dans src/groupShare.ts — fonctions pures, hors du composant. La purge est une question de sécurité, pas de confort, et c'est ce qui a classé l'item L : trois points ont dû être tranchés avec l'utilisateur avant d'écrire, et le premier l'a été après vérification, pas d'après la fiche.

Le coffre-fort ne marche pas comme cette fiche le supposait

Détaillé en piège #78, et l'essentiel tient en une phrase : un profil ne porte aucune référence au vault, c'est le vault qui est indexé par les coordonnées de connexion. Il n'y avait donc rien à « dé-référencer » ; ce qui compte est options.password, un mot de passe réellement stocké dans le profil. Seule vraie référence : privateKeys[] peut contenir vault://<id>, opaque et inutile ailleurs — retiré, car le garder donne un profil qui échoue à trouver sa clé sans rien dire.

Cherché avant d'écrire, comme le veut la règle : Tabby n'a aucun export/import de profils. Rien à réutiliser ici.

Ce que le profil porte vraiment, et que la fiche n'énumérait pas

Les trois arbitrages

  1. Deux niveaux, choisis au geste, et non un réglage : deux entrées au clic droit. « Copier la structure (JSON) » retire les secrets et rien d'autre — celle qui se recolle à l'identique entre ses propres machines. « Copier sans les identifiants » retire en plus le nom d'utilisateur, les clés et la route ; host et port restent, une structure sans hôte ne se recollant pas utilement. Le libellé dit « identifiants » et non « coordonnées » précisément pour ça.
  2. Les scripts de login sortent toujours, et l'export les compte : celui qui colle apprend qu'il y en avait deux et qu'il devra les refaire, sans qu'on lui livre leur contenu. Un script se réécrit vite, un secret parti ne se rattrape pas.
  3. La collision se demande, et seulement quand elle a lieu. La modale offre « Coller à côté » (suffixe « X - Copie », celui de Clone Express) et « Fusionner ». Entrée tombe sur la première : celle qui ne peut rien écraser.
La purge tourne aussi à l'entrée

Ce que le JSON dit avoir retiré ne vaut rien : c'est du texte pris au presse-papiers, éditable à la main et peut-être écrit ailleurs. parsePayload() repurge donc tout ce qui entre, et garde deux comptes séparésremoved, ce que l'export annonce (la seule chose qui puisse dire « il y avait un mot de passe ici, à ressaisir », puisque par construction il n'en reste rien à compter), et strippedOnImport, ce que cette seconde passe a dû retirer elle-même. Vide sur un export maison ; non vide, une notice rouge signale que le JSON portait plus que son en-tête n'admettait.

Ce point a failli être perdu : la première version ne gardait qu'un rapport, re-dérivé à l'import — donc toujours vide, puisque l'export avait déjà tout retiré. Le « et le dire » demandé par l'utilisateur ne se serait jamais affiché, et aucun test fonctionnel ne l'aurait montré.

Ce que le banc a trouvé, et qu'une relecture n'aurait pas vu

La purge étant du code pur, elle a été vérifiée hors de Tabby avant toute passe manuelle : 59 contrôles, refus et entrées malformées compris. Elle a trouvé un trou — defaults traité au mauvais niveau, piège #77 — par lequel un script de login de dossier sortait entier. Ce qui l'a révélé n'est pas la lecture mais le comptage : un mot de passe classé « champ sensible » au lieu de « mot de passe », donc retiré par le filet et non par la règle qui aurait dû s'en charger.

Limites assumées

Le dépôt est public : aucun exemple de ce chantier ne porte de donnée réelle (app.exemple.fr).

Livré le 2026-08-03 · Journal : « G1 — duplication, snippets, notes et partage de groupe (2026-08-03) » (2026-08-03) · Pièges issus du chantier : #76, #77, #78 · Commits : e2cea77 (commits du lot de livraison, partagés entre ses chantiers)

Notes de profil (Mémento contextuel) ✅ Fait (2026-08-03)

Zone de texte légère ou bulle d'aide éditable propre à chaque profil pour y consigner des notes d'exploitation (commandes de relance, rappels de maintenance, numéros de tickets).

Fait — testé et validé par l'utilisateur le 2026-08-03

Commit e2cea77. Clé profileNotes, une chaîne par id, sur un profil comme sur un dossier.

Livré le 2026-08-03 · Journal : « G1 — duplication, snippets, notes et partage de groupe (2026-08-03) » (2026-08-03) · Pièges issus du chantier : — · Commits : e2cea77 (commits du lot de livraison, partagés entre ses chantiers)

Indicateur de santé (Ping live)

Option pour afficher une micro-pastille colorée (vert/orange/rouge) à côté des sessions actives représentant la latence réseau actuelle via un ping régulier en tâche de fond (désactivable).

Fait — testé et validé par l'utilisateur le 2026-08-03

Branche feat/ping-live, détail dans AI-HISTORY. Réglage pingIntervalSeconds, 0 par défaut : une mesure est une requête adressée à un vrai serveur, elle ne part donc pas sans qu'on le demande.

Ce que « ping » veut dire ici a été tranché, pas supposé. Ni Tabby ni russh n'exposent de temps d'aller-retour, le chiffre est donc produit — et le moyen choisi définit ce que la pastille signifie. Retenu : un stat() chronométré sur le canal SFTP déjà ouvert de la session, qui mesure l'aller-retour chiffré de cette session, celui qu'on ressent en tapant. Les deux autres voies ont été écartées pour des raisons constatées, pas par préférence : le ping ICMP mesure l'hôte et se fait couramment filtrer, donc rouge sur un lien sain ; la sonde TCP ouvre puis abandonne une connexion avant authentification à chaque tour, ce qui écrit dans les logs du serveur et peut faire bannir par son propre fail2ban.

Écarts au design ci-dessus, tous issus de la passe de test :

Deux limites assumées. Le canal SFTP est partagé avec les transferts réels : une sonde envoyée pendant un gros transfert fait la queue derrière ses données et lit haut — c'est la session qui répond lentement, pas une mesure fausse. Et un serveur sans sous-système SFTP n'est pas mesurable du tout : sa pastille reste grise et il n'est plus sollicité.

Piège #69 consigné au passage : réécrire un title détruit l'infobulle affichée, donc une valeur qui se rafraîchit scintille sous le curseur.

Livré le 2026-08-03 · Journal : « Latence et âge des sessions actives (2026-08-03) » (2026-08-03) · Pièges issus du chantier : #69 · Commits : 161074c, a9c56aa, 68e81c7, 3c17f14, a8f5c59

Robustesse — vérifier le couplage à l'hôte au démarrage ✅ Fait (2026-08-03)

Fiche élargie le 2026-08-03, sur une observation de l'utilisateur : le plugin tire sa puissance d'une intégration profonde à Tabby, et c'est la même chose qui le rend cassable par une mise à jour mineure de l'hôte. Elle ne portait jusque-là que sur l'édition de profil — cas devenu sans objet, le couplage ayant été supprimé plutôt qu'instrumenté. Livré sur refactor/edition-profil-modale-directe puis feat/controle-compatibilite-hote.

Fait — 18 contrôles CDP verts le 2026-08-03

Deux mécanismes, l'un statique et l'autre en fonctionnement, dans src/hostCompat.ts :

Signalement une fois, jamais en cascade : verifyHost() tourne à ready$ et pas depuis sync(), qui se rejoue à chaque config.changed$ et transformerait un hôte cassé en flux de toasts. La sonde de dédoublement mémorise la classe déjà signalée. Et le message part aussi en console.error : une notification disparaît en quelques secondes, or c'est précisément le genre de panne qu'on revient diagnostiquer plus tard.

Vérifié au test : hôte sain, aucun bruit ; conteneur neutralisé, verdict fatal, une notification, détail en console, sidebar non montée, et l'hôte laissé sans classe résiduelle — un plugin qui renonce n'a pas à continuer de masquer un morceau de l'interface de Tabby ; restauration, la sidebar revient et le verdict retombe. La sonde de dédoublement a été exercée avec un imposteur (bon nom, mauvaise identité) : elle se déclenche, une seule fois, et l'imposteur n'entre pas dans les sessions actives.

Ce que ce contrôle ne fait pas, volontairement

Il ne rend pas le plugin plus compatible — il ne réduit ni ce qui casse ni la fréquence. Il change le mode d'échec : d'un non-événement (une sidebar qui n'apparaît pas, un panneau vide, un instanceof faux à jamais) vers quelque chose qui se lit. C'est un gain de diagnostic, à ne pas relire plus tard comme une promesse de solidité.

Le détail par fonctionnalité n'est pas encore consommé par le code : retirer un bloc précis (masquer la bascule SFTP quand SFTPPanelComponent a disparu) suppose un moyen d'éteindre un bloc, que construit Interrupteurs par bloc. Stocker la liste dès maintenant aurait posé un second mécanisme inutilisé en concurrence de celui qui arrive. C'est le point de jonction des deux chantiers.

Ce à quoi le plugin est réellement couplé

Inventaire fait sur src/ avant d'écrire cette fiche, pour que le chantier porte sur des points constatés et non sur une inquiétude générale. Quatre natures de couplage, très inégalement risquées — les traiter ensemble reviendrait à sur-protéger les deux dernières :

NatureCe qui casse, et comment ça se voit
DOM non versionné de l'hôte mount.service.ts : .window.h-100.d-flex Le plus dur, et toujours muet : mount() fait un return nu si le conteneur est introuvable, donc la sidebar n'apparaît pas et rien ne le dit. C'est désormais le seul point de cette nature — l'édition de profil, qui pilotait le DOM de tabby-settings, est passée par l'API le 2026-08-03 (fiche, § fragile-1).
Classes empruntées à tabby-ssh Sous-classement de SFTPPanelComponent, instanceof SSHTabComponent, SFTPSession Ces classes sont exportées à l'exécution (§ fragile-3) — le risque n'est pas leur absence mais leur dédoublement : un second tabby-ssh dans le node_modules du plugin rend tout instanceof faux sans une erreur (#34). Un typage augmenté à la main (tabby-ssh-augment.d.ts) ment en plus au compilateur (#17).
Encapsulation CSS contournée SCSS injecté en global (imposé par #3) Débordement sur l'hôte plutôt que rupture. Traité une fois de bout en bout au ménage avant release : chaque classe confrontée aux builtin-plugins/ installés, cadrage par le tag (#71). C'est une discipline à tenir, pas un contrôle à automatiser.
API publiques de tabby-core fileTransferStarted$, PlatformService, ConfigProvider, HotkeyProvider Membres publics, consommés par Tabby lui-même — appRoot s'abonne au même flux que nous. Rien à durcir ici : le noter évite de dépenser le chantier sur ce qui est déjà contractuel.

Ce qui manque : un échec qui se voit

Le défaut commun aux deux premières lignes n'est pas l'absence de try/catch — les chemins SFTP en sont couverts — mais le fait qu'un couplage rompu produise un non-événement : une sidebar qui n'apparaît pas, une modale qui ne s'ouvre pas, un panneau vide. Rien à lire dans la console, rien à signaler à l'utilisateur, et un diagnostic qui repart de zéro à chaque fois.

Le contrôle ne doit pas devenir le troisième endroit où le couplage est décrit

Les points de contact sont déjà listés dans les points de veille d'AI-CONTEXT, qui existent précisément pour être revérifiés après une mise à jour. Une liste de préconditions dans le code en ferait une seconde source, qui dérivera. La forme à viser est un point unique dans le code — une table de préconditions nommées — que la fiche d'AI-CONTEXT désigne au lieu de la recopier.

Points à trancher avant de coder

Le cas d'origine : édition de profil ✅ Sans objet (2026-08-03)

Cette fiche demandait à l'origine d'envelopper clickNativeProfileRow() pour qu'un changement de DOM dans tabby-settings affiche un message clair plutôt que d'échouer en silence. Le pilotage DOM a été supprimé au lieu d'être instrumenté — voir Édition de profil par l'API. Il n'y a plus rien à durcir ici.

Ce que ça enseigne au reste du chantier

Le premier réflexe devant un couplage fragile a été de mieux surveiller la casse. La bonne réponse, dans ce cas-là, était de retirer le couplage — et elle a coûté moins cher que le garde-fou qu'on s'apprêtait à construire. Avant d'instrumenter un point de contact, poser la question dans cet ordre : peut-il disparaître ? Sinon seulement, comment le rendre bruyant.

Livré le 2026-08-03 · Journal : « Le point le plus fragile du plugin a été supprimé, pas consolidé (2026-08-03) » (2026-08-03) · Pièges issus du chantier : — · Commits : e386b94, 3ecac9b (commits du lot de livraison, partagés entre ses chantiers)

Combler les points de veille restants ✅ Fait (2026-08-03)

Ouvert puis mené le 2026-08-03, en session dédiée, à la demande de l'utilisateur. Suite directe du contrôle de compatibilité : celui-ci avait rendu bruyants les couplages qu'il couvre, cette fiche a traité ce qu'il ne couvrait pas. Branche refactor/points-de-veille.

État après le chantier

Sévérité établie sur deux axes — ce que la panne emporte et comment elle se manifeste. Le second pèse plus lourd : une panne bruyante sur une grosse fonctionnalité coûte moins qu'une panne muette sur une petite. C'est le second axe, et lui seul, que ce chantier a déplacé.

PointCe qui casseDétectionSévérité
Classes tabby-ssh — panneau sous-classé, SSHTabComponent, SFTPSession SFTP, sessions actives, tunnels Compilation, précondition, et sonde du #34 🟠 Moyenne — la plus haute qui reste
EditProfileModalComponent — noms des deux entrées, que l'augmentation de types affirme sans rien prouver Création et édition de profil Message au démarrage, notice, et refus d'ouvrir (était : aucune) 🟡 Faible (était 🔴)
Téléchargements servis par nous « Download » du menu contextuel Deux appuis sur trois retirés ; le troisième est notre propre code 🟡 Faible (était 🟠)
writeProfileGroup() et sœurs Re-parentage de dossier Testable sur grp-zzz-test-* 🟡 Faible
.window.h-100.d-flex dans mount.service.ts La sidebar entière Fatale, notifiée et journalisée depuis le 2026-08-03 🟡 Faible
Clone non garanti de getProfileGroups()/buildGroupTree() Intégrité de config.yaml Point unique, plus une sonde d'identité une fois par session 🟢 Très faible (était 🟠)
SettingsTabComponent.activeTab Le seul repli « provider introuvable » Visible immédiatement 🟢 Très faible
profile-tree non exporté Rien ⚪ Nul — une simplification manquée, pas un risque
Ce que ce chantier n'a pas fait

Aucun de ces contrôles ne rend le plugin plus compatible. Une version de Tabby qui renomme partialProfile casse la création et l'édition de profils exactement comme avant ; la seule différence est qu'elle le dit. Ce qui a changé, c'est le mode d'échec — même réserve que pour le contrôle de compatibilité, et elle vaut d'être répétée parce qu'un tableau où plus rien n'est rouge se relit facilement comme un tableau de points supprimés. Deux points l'ont vraiment été, et c'est dit explicitement ci-dessous ; les autres ont seulement appris à parler.

fragile-7 — le point rouge, et le piège de la piste qui semblait évidente

Le contrôle de compatibilité vérifiait que EditProfileModalComponent existe, jamais que ses entrées s'appellent encore partialProfile et profileProvider. Cette panne-là était muette des deux côtés : TypeScript se taisait parce que l'augmentation affirme ces noms, Angular parce qu'assigner une propriété inconnue à une instance est parfaitement légal.

Deux gestes, dans l'ordre que ce projet s'impose — d'abord réduire, ensuite seulement surveiller :

La piste écrite dans cette fiche était fausse, et l'aurait été en silence

Cette fiche proposait de lire ɵcmp.inputs, en demandant d'établir « la forme exacte de la table (alias public → nom de propriété, ou l'inverse) ». Bien lui en a pris : la table est indexée par le nom public, et la modale déclare @Input('profile') partialProfile. Une précondition qui y aurait cherché la clé partialProfile aurait échoué à tous les démarrages, sur un Tabby parfaitement sain — c'est-à-dire qu'un contrôle ajouté pour rendre une panne visible aurait fabriqué une fausse panne, ce qui est pire que le silence qu'il remplaçait.

Deux corrections en découlent, consignées au piège #75. reflectComponentType() est API publique depuis Angular 14.1 : la réserve de la fiche — « un champ préfixé ɵ n'est pas contractuel non plus, donc le remède partage la nature du mal » — tombe d'elle-même, il n'y avait pas lieu de lire un champ interne. Et sa table se lit par propName et non templateName, contrairement à l'exemple de la documentation d'Angular, qui intervertit les deux : c'est l'implémentation qui a tranché, pas le commentaire.

Vérifié en conditions réelles, et surtout dans les deux sens : la modale s'ouvre et porte bien le profil qu'on lui passe ; et avec un nom volontairement faux, recompilé pour l'occasion, le démarrage sort le message attendu, la notice s'affiche, l'ouverture est refusée — pendant que la sidebar et son arbre restent parfaitement fonctionnels, la précondition n'étant pas fatale. Un contrôle qu'on n'a pas vu se déclencher ne prouve rien.

fragile-5 — la discipline devenue structure, et une supposition enfin mesurée

readProfileGroups() (src/profileGroups.ts) est désormais le seul appel du plugin à getProfileGroups(), et il rend un clone par construction : les deux appelants recopiaient les mêmes trois lignes et le même commentaire d'avertissement, et rien n'obligeait un troisième à le faire. Même raisonnement qu'isSSHTab() — une convention transformée en point unique.

La mesure a rapporté davantage que prévu : Tabby clone déjà. getSyncProfileGroups() rend deepClone(config.store.groups), et une comparaison d'identité sur l'app en marche confirme qu'aucun groupe ne revient comme le même objet. Le structuredClone() du piège #12 est donc, à ce jour, une ceinture par-dessus des bretelles — il reste, parce que les bretelles sont un détail d'implémentation d'une méthode que personne n'exporte et que ce qu'elles retiennent est un config.yaml corrompu. S'y ajoute une sonde qui compare cette identité une fois par session et le signale si la garantie disparaît : la supposition de la fiche est devenue un fait vérifié à chaque démarrage.

fragile-8 — deux appuis retirés plutôt que surveillés

Passe réelle sur le serveur VPN, dans /tmp/zzz-test-dl/ supprimé après coup, sur un fichier au contenu déterministe (seq 1 20000, 108 894 octets) : le chemin part de downloadItem() — ce que Tabby appelle vraiment —, l'offre HTTP locale est émise, répond 200 avec le bon Content-Length, et les octets reçus sont identiques à l'attendu. Trois contre-tests dans l'autre sens : un lien symbolique reste sur la voie héritée, un will-download installé pour l'occasion y renvoie aussi, et son retrait rend la main à la voie HTTP.

Aucune boîte native n'a été ouverte de toute la passe : le clic sur l'ancre est intercepté et startDownload() neutralisé — un dialogue natif fige le renderer, CDP compris (#65).

Ce qui reste ouvert, et pourquoi c'est raisonnable

Livré le 2026-08-03 · Journal : « Les points de veille restants : deux appuis retirés, le dernier point rouge rendu bruyant (2026-08-03) » (2026-08-03) · Pièges issus du chantier : #75 · Commits : 0d70d05

Édition de profil par l'API plutôt que par le DOM ✅ Fait (2026-08-03)

Issu du chantier robustesse ci-dessus : en inventoriant le couplage à l'hôte, le point présenté comme le plus fragile du plugin s'est révélé être un contournement inutile. Branche refactor/edition-profil-modale-directe.

« Éditer… » du menu contextuel ouvrait l'onglet Paramètres → Profils, dépliait tous les dossiers par des clics successifs, retrouvait la ligne du profil en comparant le texte d'un span, la cliquait, puis sondait .modal-content toutes les 300 ms pour savoir quand refermer l'onglet. Le commentaire qui portait ce code affirmait qu'aucune API publique n'existait. C'était faux, et contredit par le même fichier : « Nouveau profil… » ouvrait déjà EditProfileModalComponent par ngbModal.open().

Testé par pilotage CDP le 2026-08-03 — 15 contrôles verts

Passe autonome par l'injecteur Angular (jamais de clic simulé), sur un groupe et un profil jetables zzz-test créés puis retirés dans le même geste — config.store revenu à son compte exact, aucun résidu, et aucune erreur ni avertissement dans la console du renderer.

Couvert : la modale ouverte est bien EditProfileModalComponent et rendue dans le DOM ; elle reçoit un clone et non l'objet vivant ; le provider est résolu et transmis ; aucun onglet Paramètres n'est ouvert au passage ; une annulation n'écrit rien ; un enregistrement persiste le nom et le port sans perdre les champs non touchés, sans dupliquer le profil, en préservant type et groupe ; et le cas « provider introuvable » ne montre aucune modale, notifie, et retombe sur Paramètres → Profils.

Non couvert par l'automatisation, à voir à l'œil : le rendu de la modale et le clic réel sur « Enregistrer ». Le test appelle save(), qui est exactement ce que le bouton déclenche, mais il ne dit rien de l'apparence.

Une limite héritée, ni corrigée ni aggravée

writeProfile() ne fait rien quand le profil n'est pas dans config.store.profiles — le cas d'un profil fourni par un provider (piège #74). Éditer un tel profil se perd donc en silence, exactement comme dans l'onglet Profils natif. Ce n'est pas une régression de ce chantier et ça n'a pas été traité ici : le corriger demanderait de décider ce que « modifier un profil que Tabby regénère » veut dire, ce qui est un autre sujet.

Livré le 2026-08-03 · Journal : « Le point le plus fragile du plugin a été supprimé, pas consolidé (2026-08-03) » (2026-08-03) · Pièges issus du chantier : — · Commits : e386b94 (commits du lot de livraison, partagés entre ses chantiers)

Interrupteurs par bloc de la sidebar ✅ Fait (2026-08-03)

Proposé par l'utilisateur le 2026-08-03. Motif : garder la puissance du « tout-en-un » sans l'imposer — une interface épurée à qui n'a besoin que des workspaces et des favoris, sans rien retirer à qui veut tout. Branche feat/interrupteurs-par-bloc.

Fait — 46 contrôles CDP verts le 2026-08-03

Six clés plates sous sidebarPlus (showActiveSessions, showTunnels, showSftp, showTransfers, showWorkspaces, showFilter), toutes à true par défaut. Cinq blocs de premier rang : showTransfers est subordonné à showSftp — voir l'encadré sur le gestionnaire de transferts plus bas.

Éteindre coupe la source, pas seulement la vue — c'est tout le chantier :

Retombées quand un bloc s'éteint, réunies dans reconcileHiddenBlocks(), appelé avant le rechargement de l'arbre : la vue SFTP revient aux profils, le workspace actif retombe sur « Tous », le filtre en cours est vidé. Chacune évite un état à la fois inatteignable et inexplicable — un workspace qui filtre sans barre pour en sortir, un filtre qui masque sans champ qui dise pourquoi.

Garde-fou posé par l'utilisateur : désactiver n'écrase jamais une configuration

Demandé en cours de chantier, et il y avait bien un cas — trouvé par le test écrit pour le vérifier, pas par relecture. Éteindre les workspaces fait retomber l'affichage sur « Tous », et loadTreeItems() persistait cette valeur dans localStorage.sidebarPlusActiveWorkspace en fin de course : le workspace réellement choisi était perdu, et rallumer le bloc ne le rendait pas.

Corrigé en deux endroits — la retombée assigne activeWorkspaceId en mémoire au lieu de passer par selectWorkspace(), et la persistance de loadTreeItems() est conditionnée au bloc actif. Le rallumage relit la valeur mémorisée. La règle générale à retenir : un interrupteur ne persiste que sa propre valeur ; tout état qu'il se contente de cesser d'afficher lui survit intact.

Vérifié par empreinte : toute la configuration hors interrupteurs — profils, groupes, workspaces avec leurs masquages, favoris et ordres, tunnels configurés d'un profil, états de repli — bit à bit identique pendant l'extinction des six blocs et après leur rallumage.

Page de réglages refondue dans le même geste

Trois règles posées par l'utilisateur en cours de route, qui avancent au passage l'agencement demandé par Options & polissage UI :

Les options d'un bloc sont indentées sous son interrupteur, reliées par un filet vertical — sans quoi les six blocs et leurs réglages se lisaient comme une liste plate de réglages sans lien.

Ce que l'interrupteur ne fait pas

Un bloc que l'hôte ne peut plus porter (contrôle de compatibilité) s'éteint aussi, mais sans toucher à la case : elle reste telle que l'utilisateur l'a laissée, et la page affiche « indisponible sur cette version de Tabby ». Réécrire le réglage pour refléter un hôte cassé perdrait le choix le jour où l'hôte le fournit à nouveau. C'est le point de jonction annoncé entre les deux chantiers, réalisé par hostSupports().

Le gestionnaire de transferts est une option de la vue SFTP ✅ Fait (2026-08-03)

Demandé par l'utilisateur après une première passe où c'était un bloc de premier rang : les transferts viennent du SFTP, les deux vont donc ensemble. showTransfers n'a plus d'effet que si showSftp est allumé, et son interrupteur vit dans les options de ce bloc.

Le corollaire, qui n'était pas dans la demande

Le registre reflète tous les transferts de l'application, y compris ceux du panneau SFTP natif et des autres plugins. Éteindre la vue SFTP les rend donc invisibles dans la sidebar — et comme hideNativeTransfersMenu est coché par défaut, l'utilisateur se retrouvait sans aucun affichage de transfert.

Corrigé du même geste : le masquage du menu natif est suspendu dès que notre panneau ne s'affiche plus, pour l'une des trois raisons possibles (plugin éteint, vue SFTP éteinte, panneau éteint). Le réglage garde sa valeur, seul son effet est suspendu. C'est le prolongement d'un principe déjà posé au montage : un plugin n'a pas à masquer un morceau de l'interface de l'hôte qu'il ne remplace plus.

Disposition de la page : les blocs sans réglage en tête

Demandé par l'utilisateur. Tunnels, workspaces et barre de filtrage n'ont pas d'options propres : ils sont groupés et resserrés en haut de la page, avant les deux blocs qui déplient une liste — sessions actives et vue SFTP. Les espacer comme ces derniers laissait croire qu'ils avaient aussi des réglages à déplier.

Un interrupteur par bloc, dans l'onglet de réglages : sessions actives, tunnels actifs, bascule SFTP, gestionnaire de transferts, workspaces, barre de filtrage.

Livré le 2026-08-03 · Journal : « Chaque bloc de la sidebar s'éteint, et son extinction coupe la source (2026-08-03) » (2026-08-03) · Pièges issus du chantier : — · Commits : 12ad2b7, 0696799

Ménage avant release soignée ✅ Clos (2026-08-03)

Chantier clos le 2026-08-03

Les six points ont trouvé leur réponse — quatre corrigés, un tranché sans changement (LICENSE), un déjà fait avant que la fiche ne le dise. Rien ici ne bloque plus une publication npm. Ce qui reste à faire avant de publier n'appartient plus à ce chantier : c'est la passe de test d'une release, pas du ménage.

Livré le 2026-08-03 · Journal : « Revue générale, remise en ordre des documents, et intégrité des données (2026-08-02) » (2026-08-02), « Le build de publication n'embarque plus de source-map (2026-08-03) » (2026-08-03), « Le SCSS ne déborde plus sur Tabby, et les icônes ne sont plus chargées au démarrage (2026-08-03) » (2026-08-03) · Pièges issus du chantier : #71, #72, #73 · Commits : d9d291b, 9cf21dc, 551d241, fb4b719 (commits du lot de livraison, partagés entre ses chantiers)

Édition d'un fichier atteint par un lien symbolique ✅ Fait (2026-08-02)

Livré et testé sur un serveur réel le 2026-08-02

Branche fix/liens-symboliques. Le lien est résolu une fois, par readlink() puis readRemoteEntry() — jamais stat() — et c'est la cible résolue qui est éditée, chemin compris. Les quatre conséquences tombent ensemble : mode réel donc copie inscriptible, chmod de la cible avec son propre mode, empreinte de fraîcheur prise là où les changements ont lieu, et le lien n'est plus touché puisqu'on n'écrit plus dessus. Chaîne bornée à huit sauts, cible introuvable annoncée.

Passe réelle menée sur un serveur de l'utilisateur, dans un répertoire jetable sous /tmp, supprimé après coup : sept vérifications sur sept, dont le round-trip complet — téléchargement, modification, renvoi — au terme duquel le lien existe toujours et la cible a gardé son 0o640.

La contre-preuve a été mesurée sur ce même serveur : stat() du lien rend bien un mode 0o0 et une date au 1er janvier 1970, et le chmod qu'appliquait l'ancien chemin aurait bien été 0o777. Le défaut n'était pas théorique.

Effet de bord acquis : le dernier stat() d'observation de src/ disparaît. createFileFromMenu() testait la disponibilité d'un nom par stat(), qui suit les liens — un lien cassé du même nom faisait croire le nom libre, et la création écrasait le lien.

Trouvé à la revue de code du 2026-08-02, et vérifié pas à pas. Un seul geste — double-cliquer une entrée qui est un lien vers un fichier, cas courant sur un serveur (sites-enabled/, un .env lié…) — enchaîne trois conséquences, dont une qui touche la sécurité du fichier distant.

1. Le mode part à 0, donc la copie locale est créée en lecture seule

openEntry() résout le lien par sftp.stat(), seul appel d'observation qui subsiste dans src/ — et stat() rend un mode 0 (piège #50). Ce 0 est étalé dans l'objet passé à l'éditeur, puis transmis à platform.startDownload(name, mode, …), dont l'implémentation Electron fait fs.open(path, 'w', mode). La copie locale atterrit donc en 444 : l'éditeur l'ouvre et refuse d'enregistrer.

Accessoirement, le test de type du piège #45 sur le résultat de ce stat() est mort — mode & S_IFMT vaut toujours 0. Le garde-fou tient par isDirectory seul, que russh réaffecte à la main.

2. Si l'éditeur force l'écriture, le fichier distant finit en 777

VS Code et Notepad++ proposent d'écraser un fichier en lecture seule. L'envoi part alors, et le mode restauré après coup est celui qu'a lu remoteStamp() — qui interroge fullPath, c'est-à-dire le lien, pas sa cible. Un lien symbolique a un mode 0o120777 ; & 0o7777 en fait 0o777, et c'est ce chmod qui est appliqué. Le fichier devient accessible en écriture à tout le monde sur le serveur, silencieusement — le mécanisme du piège #53, correct pour un fichier ordinaire, se retourne ici.

S'y ajoute que SFTPSession.upload() fait unlink de la cible avant de renommer le temporaire par-dessus : le lien lui-même est détruit et remplacé par un fichier régulier.

3. La détection de conflit ne peut pas fonctionner

changed() compare taille et date de fullPath — donc du lien, dont ni l'une ni l'autre ne bouge quand la cible change. Pour tout fichier ouvert par un lien, la modale de conflit livrée le 2026-08-01 ne se déclenchera jamais.

Piste de correctif, à arbitrer : résoudre le lien une fois, par readlink() + readRemoteEntry() — jamais stat() — et faire travailler tout le reste de la chaîne sur le chemin de la cible, en ne gardant le nom du lien que pour l'affichage. Cela règle les trois points d'un coup : mode réel, donc copie inscriptible ; chmod sur la cible avec son propre mode ; empreinte de fraîcheur prise là où les changements ont lieu. Reste à décider ce qu'on fait de la destruction du lien par upload() — écrire dans la cible résolue l'évite, mais change ce que « renvoyer ce fichier » veut dire.

Livré le 2026-08-02 · Journal : « Revue générale, remise en ordre des documents, et intégrité des données (2026-08-02) » (2026-08-02) · Pièges issus du chantier : — · Commits : b4159f1

Cycle de vie : démontage, fenêtres multiples, offres de glisser ✅ Fait (2026-08-02)

Livré le 2026-08-02, deux points testés en conditions réelles

Branche fix/cycle-de-vie. Les sept points listés ci-dessous sont traités.

Testé — le démontage : éteindre puis rallumer le plugin deux fois de suite laisse exactement une sidebar dans le DOM et une vue attachée, là où l'ancien code en laissait une morte à l'écran puis en ajoutait une seconde. Et la purge des temporaires : un dossier dont le processus propriétaire est mort est bien supprimé au démarrage, un dossier dont le processus vit est bien conservé — c'est ce second cas que l'ancien repère temporel manquait, effaçant les copies d'une fenêtre ouverte plus tôt.

Non exercé en conditions réelles, faute de scénario reproductible à peu de frais : la revalidation du Content-Length au dépôt, et la promesse de démarrage du serveur quand la socket refuse de se lier. Les deux sont des chemins d'erreur ou de course ; le code est en place, aucune passe ne l'a exercé.

Choix à connaître : la purge décide par « le processus nommé dans le dossier vit-il encore ? », pid lu dans le nom du dossier. Un pid recyclé par un processus sans rapport ferait garder un dossier un run de trop — c'est le côté inoffensif du compromis, cette classe existant pour éviter d'abandonner des copies, jamais pour risquer d'en supprimer une en cours d'usage.

Défauts de cycle de vie relevés à la même revue. Aucun n'est déclenché par un usage courant ; ils sont réunis ici pour ne pas être redécouverts un par un.

Livré le 2026-08-02 · Journal : « Revue générale, remise en ordre des documents, et intégrité des données (2026-08-02) » (2026-08-02) · Pièges issus du chantier : — · Commits : cfe6010

Transferts par l'API officielle, fraîcheur des copies et purge des temporaires

Demandé par l'utilisateur le 2026-08-01, en réponse à la lenteur du drag-out sur un dossier et à une perte de fraîcheur rencontrée à l'usage. Principe posé à cette occasion, et valable au-delà de ce chantier : si un moyen officiel existe, on le réutilise et on l'améliore — une implémentation maison doit être justifiée par une limite réelle et constatée de l'officiel.

1. Repasser les transferts par PlatformService

sftpLocalTransfer.ts existe sur une prémisse fausse : son en-tête affirme que startDownload()/startUpload() ouvrent forcément un sélecteur de fichier. L'app installée accepte un chemin imposé et n'ouvre alors aucun dialogue (piège #48).

2. Fraîcheur des copies locales et détection de conflit

Une copie périmée est resservie sans un mot

Constaté à l'usage par l'utilisateur le 2026-08-01, sur un fichier édité localement, renvoyé au serveur, puis modifié sur le serveur : c'est l'ancienne copie locale qui lui a été rendue. Deux caches sans invalidation : la Map ready de SftpDragOut, et la réouverture d'une session existante dans SftpRemoteEditor.edit() — cette dernière délibérée (ne pas écraser du travail non sauvegardé), mais sans la vérification qui la rendrait sûre.

Le risque symétrique est plus grave et jusqu'ici assumé : à l'enregistrement, aucune détection de conflit. Un fichier modifié à distance pendant qu'il est ouvert est écrasé sans avertissement.

3. Purge des fichiers temporaires

Demandé par l'utilisateur le 2026-08-01. Aujourd'hui les copies de tabby-better-sidebar-drag et tabby-better-sidebar-edit ne disparaissent qu'au dispose() du panneau ; tout ce qui suit un crash, un taskkill ou une fermeture brutale reste sur disque indéfiniment.

4. Lenteur du téléchargement d'un dossier

Signalé le 2026-08-01 : 26 fichiers / 11 Mo, plus d'une minute, là où le téléchargement natif du même dossier est quasi instantané. Diagnostic partiel, à ne pas considérer comme établi — deux mesures faites : un fichier de 10 Mo par double-clic (donc par le transfert maison, mais hors geste de drag) est quasi instantané, ce qui disculpe le transfert lui-même. Restent en lice la boucle récursive strictement séquentielle de downloadDirectory() et le gel du renderer pendant le geste (piège #49). Le test qui tranche : glisser un dossier puis relâcher immédiatement, et chronométrer la fin de la préparation. Ne pas optimiser avant d'avoir la réponse.

Fait — testé et validé par l'utilisateur le 2026-08-01

Les quatre volets sont livrés. Ce que la passe de test a réellement produit dépasse le chantier prévu : trois défauts de fond ont été trouvés par les mesures, pas par le plan.

Vérifié en test : permissions conservées, y compris après un chmod fait pendant l'édition ; modale de conflit avant tout écrasement, refus laissant le distant intact ; réouverture actualisée ; téléchargement d'un dossier passé d'une minute à quasi instantané avec intégrité contrôlée (11 263 335 octets de part et d'autre) ; purge au démarrage et à la fermeture.

Écart assumé : le glisser-déposer sert une copie encore valide selon la liste affichée, puis prévient si le serveur la dément — dragstart étant synchrone, aucun appel réseau ne peut le précéder.

Livré le 2026-08-01 · Journal : « Transferts officiels, fraîcheur des copies, purge et performance de la liste (2026-08-01) » (2026-08-01) · Pièges issus du chantier : #48, #50, #52, #53 · Commits : 1a516b8, aedc8a7, 9e7a7c8

Performance de la liste SFTP et rafraîchissement automatique

Signalé en test le 2026-08-01 : un dossier très peuplé (/tmp) rendait le panneau « difficilement utilisable », et rien ne se rafraîchissait après une modification faite au terminal. Deux sujets, une seule cause de fond — ce que coûte chaque cycle de détection de changement (#54).

Fait — testé et validé par l'utilisateur le 2026-08-01

Deux réglages d'ergonomie livrés dans le même geste, tous deux issus de la passe : les notifications duraient une seconde (#51) et passent à 8 s, 15 s pour une erreur ; et la sidebar revient toujours sur Profils au démarrage — la vue SFTP n'a de sens qu'à côté de la session pour laquelle elle a été ouverte, et cette session n'existe plus après un redémarrage.

Livré le 2026-08-01 · Journal : « Transferts officiels, fraîcheur des copies, purge et performance de la liste (2026-08-01) » (2026-08-01) · Pièges issus du chantier : #51, #54 · Commits : 026dd7b, 576a5d6, d3695e8

Renommer une entrée depuis le panneau SFTP

Demandé par l'utilisateur le 2026-08-01. Entrée « Renommer... » du clic droit, entre « Ouvrir avec... » et « Supprimer », par SFTPSession.rename(), avec la modale HTML de tabby-core (PromptModalComponent) — l'invariant des confirmations en HTML vaut aussi pour les saisies.

Fait — testé et validé par l'utilisateur le 2026-08-01

Vérifié sur un fichier et sur un dossier, changement d'extension compris, ainsi que les deux refus attendus — nom contenant /, nom déjà pris — qui laissent le serveur intact.

Livré le 2026-08-01 · Journal : « Renommer une entrée depuis le panneau SFTP (2026-08-01) » (2026-08-01) · Pièges issus du chantier : — · Commits : e4f0a9d

Ctrl+Entrée insère un saut de ligne dans le terminal

Ce n'est pas une régression — diagnostic établi le 2026-08-01

Signalé comme une régression du plugin (soupçon porté sur la sélection multiple), ce n'en est pas une. Mesuré au terminal avec le literal next de readline (Ctrl+V suivi de la touche) : Entrée et Ctrl+Entrée émettent tous deux ^M — un simple CR. Ctrl+J, lui, émet bien LF.

Les deux frappes sont donc indistinguables une fois écrites sur le pty : aucune application distante ne peut les différencier, quel que soit son paramétrage. Symptôme identique en session locale et en SSH, sur des serveurs différents — ce qui exclut le transport autant que le plugin. Ne pas rouvrir ce point comme un bug de Tabby ou de l'application distante.

Ce qui rend la correction possible : la distinction existe encore dans le renderer, où l'événement clavier porte ctrlKey. La couche de raccourcis de Tabby y travaille, avant l'écriture sur le pty — c'est le seul endroit où un raccourci peut agir. L'action écrit alors le LF elle-même.

Premier usage réel de l'item « Raccourcis globaux » des options & polissage UI, jusqu'ici jamais instancié.

Fait — testé et validé par l'utilisateur le 2026-08-01

Vérifié sur les cinq points : le raccourci est présent et lié dans les réglages ; le saut de ligne s'insère en session locale et en SSH distant ; Entrée seule continue de soumettre ; la combinaison reste sans effet pendant qu'un champ de la sidebar a le focus ; et dans un onglet divisé, c'est bien le volet focalisé qui reçoit.

Livré le 2026-08-01 · Journal : « Ctrl+Entrée insère un saut de ligne (2026-08-01) » (2026-08-01) · Pièges issus du chantier : — · Commits : 02e7d24

Sessions actives visibles aussi en vue SFTP

Demandé par l'utilisateur le 2026-07-30, avec trois autres axes UX/sécurité — suppression SFTP par Suppr, éditeur forcé au double-clic, glisser-déposer sortant — et un audit de conformité (confirmations en modale HTML). Traité en premier, comme le plus simple des cinq.

Bug de régression de périmètre constaté dans le code : la section Sessions Actives, livrée et validée le 2026-07-29, se masque entièrement dès qu'on bascule sur la vue SFTP (*ngIf='!sftpMode && activeSessions.length' dans sidebarTree.component.pug). Or le design d'origine ne prévoyait aucune exception liée au mode d'affichage — la section doit rester visible et se comporter à l'identique, quel que soit l'onglet actif de la sidebar (Profils ou SFTP). Correctif : retirer la condition !sftpMode de ce *ngIf, et vérifier si le panneau des tunnels actifs porte la même restriction héritée (à corriger de la même façon si c'est le cas).

Fait — testé et validé par l'utilisateur le 2026-07-30

Commits efcf3f8 (la condition retirée) puis e054ae6 (tout le reste), détail dans AI-HISTORY. Retirer le !sftpMode ne suffisait pas : les blocs étaient placés après <sidebar-plus-sftp>, qui est flex-grow-1 dans la colonne flex, donc tout ce qui le suit est rejeté en bas de la sidebar. L'ordre du template comptait autant que la condition — les deux blocs sont désormais avant le panneau.

Trois décisions prises pendant le test, à rejuger à l'usage : les tunnels actifs sont au contraire masqués en vue SFTP (demande explicite de l'utilisateur — l'onglet SFTP n'a pas d'usage de l'état de forwarding), ce qui tranche la question laissée ouverte ci-dessus ; la liste des sessions plafonne à 4 lignes puis défile, l'en-tête et son compteur restant fixes ; le libellé d'une ligne suit désormais le renommage manuel de l'onglet quand il y en a un, avant le nom du profil, pour distinguer plusieurs sessions ouvertes sur la même machine.

Écart de comportement assumé avec Tabby : effacer un renommage (modale vidée) laisse l'en-tête d'onglet natif vide jusqu'au prochain titre dynamique, là où la sidebar retombe proprement sur le nom du profil. Constaté au test, conservé tel quel — la sidebar ne pilote pas le titre des onglets.

Livré le 2026-07-30 · Journal : « Campagne UX/sécurité du 2026-07-30 — suppression SFTP et sessions actives » (2026-07-30) · Pièges issus du chantier : — · Commits : efcf3f8, e054ae6 (commits du lot de livraison, partagés entre ses chantiers)

Suppression SFTP par touche Suppr

Demandé par l'utilisateur le 2026-07-30, explicitement limité au panneau SFTP distant — ne touche pas à l'arbre de profils/groupes, qui a son propre mécanisme de suppression au clic droit (confirmDeleteProfile/deleteGroup), non concerné par cette demande.

Fait — testé et validé par l'utilisateur le 2026-07-30

Commits efcf3f8 (le geste et la modale) et dcabce7 (garde-fous), détail dans AI-HISTORY. SFTPDeleteModalComponent n'a pas été retenu : un ConfirmModalComponent maison (NgbModal) sert à la fois la touche Suppr et l'entrée « Delete » du clic droit, ce qui referme du même geste l'écart d'audit des confirmations — les deux chemins passent désormais par la même modale HTML.

Livré le 2026-07-30 · Journal : « Campagne UX/sécurité du 2026-07-30 — suppression SFTP et sessions actives » (2026-07-30) · Pièges issus du chantier : — · Commits : efcf3f8, dcabce7 (commits du lot de livraison, partagés entre ses chantiers)

Double-clic force un éditeur de code, jamais une exécution

Demandé par l'utilisateur le 2026-07-30. Bug de sécurité/UX confirmé dans le code : SftpRemoteEditor.open() (src/sftpRemoteEdit.ts:59-61) appelle platform.openPath(), donc shell.openPath() d'Electron — ouverture avec l'application associée par l'OS. Pour un exécutable ou un script dont l'extension est associée à un interpréteur, un double-clic peut donc lancer le fichier plutôt que l'éditer.

Vérifié avant conception : @electron/remote est déjà initialisé et utilisé côté renderer dans tabby-electron/src/services/electron.service.ts (remote.dialog, entre autres) — le sélecteur de fichier natif est donc accessible depuis le plugin sans plomberie IPC supplémentaire.

Fait — testé et validé par l'utilisateur le 2026-07-30

Commit 1419073, détail dans AI-HISTORY, trois pièges consignés (#44, #45, #46). Livré conforme au plan ci-dessus sur l'essentiel — plus de openPath(), spawn explicite, « Ouvrir avec... » en échappatoire de clic droit jamais atteignable au double-clic — avec trois écarts assumés, tous issus du test :

Bug corrigé au passage, sans rapport avec ce chantier mais trouvé par lui : un lien symbolique vers un dossier partait au téléchargement au lieu d'être parcouru (« Impossible de télécharger infra ») — SFTPFile.isDirectory vient d'un champ que le protocole SFTP v3 ne transporte pas (piège #45).

Livré le 2026-07-30 · Journal : « Campagne UX/sécurité du 2026-07-30 — suppression SFTP et sessions actives » (2026-07-30) · Pièges issus du chantier : #44, #45, #46 · Commits : 1419073

Confirmations en modale HTML (audit de conformité)

Demandé par l'utilisateur le 2026-07-30 comme principe transversal. Audit fait sur notre propre code (src/) : conforme — aucune occurrence de window.confirm/window.alert/dialog.showMessageBox. Les suppressions de profil/groupe/workspace passent par un mode de menu contextuel inline en HTML (contextMenuModeconfirmDeleteProfile, etc.), et le panneau SFTP utilise déjà NgbModal/PromptModalComponent (Bootstrap) pour ses propres popups.

Écart trouvé en creusant — pas dans notre code, dans ce qu'on hérite

SidebarPlusSftpBrowserComponent sous-classe SFTPPanelComponent et hérite tel quel de showContextMenu(), dont l'entrée « Delete » (fournie par l'unique provider natif, CommonSFTPContextMenu) confirme via platform.showMessageBox() — un vrai dialogue OS, pas une modale HTML. Détail complet : piège #42. À traiter dans le même geste que suppression SFTP par Suppr ci-dessous : showContextMenu() à surcharger pour remplacer cette entrée par une ouvrant une modale HTML/NgbModal maison, partagée avec le geste Suppr.

Corrigé et validé le 2026-07-30 (efcf3f8, garde-fous dcabce7) : les deux chemins de suppression SFTP — touche Suppr et entrée du clic droit — ouvrent la même modale HTML maison. Le retrait de l'entrée native se fait par position et non par libellé traduit : l'app installée n'a pas de catalogue français pour cette clé, un test de texte y était donc inopérant.

Invariant à maintenir pour tout ce qui s'ajoute désormais : la confirmation de suppression SFTP par Suppr et celle, déjà notée dans Transferts SFTP, sur l'abandon de transferts en cours. Les sélecteurs de fichiers système utilisés par l'éditeur forcé (choix de l'exécutable, "Ouvrir avec...") ne sont pas concernés par cet invariant — ce sont des sélecteurs de fichiers, pas des confirmations.

Livré le 2026-07-30 · Journal : « Campagne UX/sécurité du 2026-07-30 — suppression SFTP et sessions actives » (2026-07-30) · Pièges issus du chantier : #42 · Commits : efcf3f8, dcabce7 (commits du lot de livraison, partagés entre ses chantiers)

Sélection multiple

Uniquement pour les profils, pas les groupes. Permet de sélectionner plusieurs profils puis de les déplacer tous ensemble vers un autre groupe en un seul geste.

Fait — testé et validé par l'utilisateur le 2026-07-29

Deux écarts au design ci-dessus, tous deux décidés en test manuel :

Placement du lot : il se pose sous la ligne visée, et en tête du dossier quand on vise la ligne d'un dossier (l'ajouter à la fin l'enfouissait sous tout le contenu existant). Le point de dépôt est déterminé par mesure directe des lignes rendues, pas par CdkDragDrop.currentIndex — voir piège #40. Pendant le glisser, les autres lignes du lot passent en visibility: hidden ; les replier hors du flux cassait le dépôt.

Cible « Sans groupe » : le clic droit y est désormais possible, avec un menu réduit à la seule entrée de déplacement — onProfileDrop() l'acceptait déjà comme cible, le menu non.

Livré le 2026-07-29 · Journal : « Sélection multiple (2026-07-29) » (2026-07-29) · Pièges issus du chantier : #39, #40, #41 · Commits : bcc92b8, a0bca09

Favoris d'icônes (distincts des « Récentes »)

Demandé directement par l'utilisateur (2026-07-28) après usage du picker d'icônes existant. Épingler une icône dans une liste séparée et permanente, indépendante de la fréquence d'usage — contrairement à « Récentes » qui s'use avec le temps (une icône peu utilisée finit par en sortir), une icône favorite reste tant qu'on ne la retire pas explicitement.

Fait — testé et validé par l'utilisateur le 2026-07-29

Commit 76085d5. Deux écarts au design d'origine ci-dessous, tous deux issus du test manuel :

Point d'architecture à connaître avant d'ajouter un menu au picker : ce menu ne passe pas par contextMenuMode, dont les modes sont mutuellement exclusifs — en utiliser un démonterait le picker sur lequel le menu agit. Il porte donc son propre état d'ouverture et ses propres coordonnées (partager contextMenuX/Y déplacerait le picker lui-même, qui en est positionné). Le recadrage, lui, reste mutualisé via clampInViewport() pour ne pas contourner le point unique du piège #30.

Non retenu faute d'être demandé : retirer des « Récentes » une icône déjà épinglée (elle apparaît donc dans les deux sections), et cloisonner les favoris d'icônes par workspace — une icône est un choix de rendu, pas un élément que le workspace montre ou masque.

Lié, déjà fait le 2026-07-28 (voir AI-HISTORY.html) : le nombre d'icônes "Récentes" affichées est passé de 5 à 20 (MAX_RECENT_ICONS), pour garder les icônes fréquentes à portée de main même avant d'implémenter ce chantier des favoris.

Livré le 2026-07-29 · Journal : « Favoris d'icônes (2026-07-29) » (2026-07-29) · Pièges issus du chantier : — · Commits : 76085d5