Journal de bord

Chantiers classés du plus récent au plus ancien. À l'intérieur d'un chantier, entrées les plus récentes en tête.

Ne jamais modifier une entrée existante

Uniquement en ajouter en tête du chantier concerné. Format d'une ligne : Date | Hash | Résumé.

Publication de la 1.0.3 (2026-08-14)

Première publication depuis la 1.0.2 : elle embarque le lot de sécurité, le toast des retraits SVG (les deux chantiers ci-dessous) et introduit le CHANGELOG.md, rétro-rempli de la 0.2.0 à la 1.0.3 et ajouté aux fichiers du paquet — la fiche #changelog voulait qu'il naisse avec la version qui le mentionne.

DateHashRésumé
2026-08-14(commit en cours) Passe de release validée par l'utilisateur (7/7) sur le paquet installé hors jonction en build de production — toast des retraits et rejet d'un SVG hostile vérifiés sur le bundle minifié avec dompurify 3.4.13. npm publish (dist-tags.latest = 1.0.3, 4,7 Mo compressés, 105 fichiers, 29 mots-clés vérifiés après coup), release GitHub v1.0.3. Le gitHead du tarball pointe le commit de version fef60c1 — commité avant le publish, leçon de la 1.0.2. Jonction de dev remise et dist/ rebuildé en dev, dernier geste du protocole.
2026-08-14fef60c1 package.json à 1.0.3, CHANGELOG.md créé (rétro-rempli depuis les releases GitHub et ce journal) et ajouté à files ; correction du décompte de mots-clés de l'entrée 1.0.2 (29, pas 28).

Lot de sécurité sans majeure — 22 → 18 vulnérabilités (2026-08-14)

Sur consigne de l'utilisateur, depuis les notes de son tableau de bord GitHub consignées en roadmap le matin même (fiche #audit-devdeps). Exécution et instruction de compatibilité menées par deux agents en parallèle, vérifiées ensuite.

DateHashRésumé
2026-08-14(commit en cours) Validé par l'utilisateur (SVG de test collé au sélecteur d'icônes sur le paquet rebuilé), fusion sur master (e5b9a55). La passe de validation a fait naître le chantier du toast, consigné ci-dessous. Fiche roadmap passée en surveillance : les 18 vulnérabilités restantes sont bloquées par Angular 15 (imposé par Tabby amont) et pug 2.
2026-08-14e37afbf npm audit fix sans --force : dompurify 3.4.13, nanoid 3.3.18, fast-uri 3.1.5, brace-expansion 2.1.4 — seul package-lock.json touché, package.json intact, builds de production et de développement verts sans warning nouveau. Compatibilité de dompurify — seul du lot embarqué dans le bundle — instruite contre l'amont : l'advisory corrigée (GHSA-55q2-fjhq-7xh7) exige IN_PLACE plus un hook de retrait, que le plugin n'utilise nulle part — non vulnérables même en 3.4.12 ; seul changement sur notre chemin, le hoisting par déplacement au lieu de clonage, sans effet sémantique.

Toast des retraits SVG (2026-08-14)

Trouvé par l'utilisateur pendant la passe de validation du lot ci-dessus : le décompte « N élément(s) retiré(s) » s'affichait dans la modale du sélecteur d'icônes, que applyIcon() ferme aussitôt l'icône appliquée — le message mourait avec elle, illisible. Demande explicite : un vrai toast, pas une ligne dans la modale.

DateHashRésumé
2026-08-14(commit en cours) Validé par l'utilisateur, fusion sur master (4d5c6a3).
2026-08-1453869ec L'avertissement passe par SidebarPlusNoticesService (toast de 8 s, empilé avec ceux de Tabby), émis juste avant la fermeture de la modale ; la ligne inline est retirée du template. La ligne d'erreur (« SVG rejeté ») reste dans la modale : sur ce chemin elle ne se ferme pas. Clé i18n existante réutilisée, aucune chaîne nouvelle (lint:i18n complet, 426 clés).

1.0.2 — référencement npm seul, aucun changement de code (2026-08-10)

Publication menée par l'utilisateur lui-même, consignée a posteriori le 2026-08-14. Le paquet remontait mal dans la recherche npm : ses six mots-clés ne couvraient pas ce qu'il fait. Seul le référencement a été amélioré — la liste keywords de package.json passe de 6 à 28 entrées (sftp-client, ssh-tunnel, drag-and-drop, workspaces, better-tabby…), tabby-plugin restant en tête pour le gestionnaire de plugins de Tabby. Aucun changement de code ni de fonctionnalité entre 1.0.1 et 1.0.2 ; dist/ n'a été rebuildé que par prepublishOnly.

DateHashRésumé
2026-08-14(commit en cours) Correction de décompte : la liste porte 29 mots-clés, pas 28 — recomptée en préparant la 1.0.3, sur une alerte de l'utilisateur. Le fond de l'entrée est inchangé.
2026-08-106fb5b64 npm publish de la 1.0.2 (dist-tags.latest = 1.0.2), tag v1.0.2. Le gitHead du tarball pointe f954bbf (publish lancé avant le commit) — sans conséquence, le contenu publié correspond bien au commit. Le clone local de ce poste, resté sur la 1.0.1, a été resynchronisé (fast-forward) le 2026-08-14.

1.0.1 — les notices tierces voyagent avec le paquet (2026-08-08)

Trouvé en répondant à une question de l'utilisateur sur les différences entre licences : le paquet npm ne contenait pas THIRD-PARTY-NOTICES.md. Le champ files ne listait que dist, et npm n'ajoute d'office que la licence et les README. Or le paquet redistribue des œuvres sous Apache 2.0 — les logos dashboard-icons et DOMPurify — dont l'article 4a demande que le destinataire reçoive une copie de la licence. Sur GitHub tout était en place ; dans le paquet installé, non.

DateHashRésumé
2026-08-08f954bbf files passe à ["dist", "THIRD-PARTY-NOTICES.md"], vérifié dans le tarball. La section dashboard-icons des notices déclare désormais que les fichiers ont été modifiés et lesquelles de leurs parties (article 4b) — épinglage du fill, préfixage des identifiants internes, scopage des blocs <style> — ce que seul le PROVENANCE.md disait, et il ne voyage pas. L'amont n'a pas de fichier NOTICE (vérifié par l'API GitHub), donc l'article 4d ne s'applique pas. Licence du projet maintenue à MIT, décision de l'utilisateur après examen : Tabby, le plugin frère et l'écosystème le sont, et la concession de brevets d'Apache 2.0 ne couvre aucun risque réel ici.

README français, parité avec le plugin frère (2026-08-08)

Question de l'utilisateur : « le README.fr.md a disparu ? ». Vérification faite dans l'historique complet — il n'a jamais existé sur ce dépôt, qui n'a connu que README.md et .AIRules/README.html. C'est le vault qui en a un. Créé donc, pour la parité entre les deux plugins.

DateHashRésumé
2026-08-08d951ab2 Traduction française intégrale du README de la 1.0.0, avec la ligne de bascule English · Français ajoutée des deux côtés (motif repris du vault). Parité contrôlée par script : mêmes 14 sections, les 23 réglages présents dans les deux fichiers. Aucune republication npm nécessaire — le paquet ne contient que dist/, README.md et LICENSE, donc ce fichier ne concerne que la page GitHub et la 1.0.0 publiée n'est pas touchée.

Publication de la V1.0 (2026-08-08)

tabby-better-sidebar@1.0.0 est sur npm et sur GitHub. La condition posée par la roadmap — une passe de test de release avant toute publication — est remplie : la passe porte sur le paquet et non sur les fonctionnalités, celles-ci ayant été validées une à une dans la journée.

DateHashRésumé
2026-08-081abcba5 Passe de release validée par l'utilisateur (15 tests sur le paquet installé hors jonction, en build de production). Ce que cette passe couvre et qu'aucune autre ne pouvait : le JIT de l'i18n sur un bundle minifié dans les trois langues, le chargement différé des chunks d'icônes, les deux modales portées le matin même, les liens du pied de page, et le collage d'un JSON hostile sur le paquet réel. npm publish (dist-tags.latest = 1.0.0, 4,7 Mo compressés, 102 fichiers) puis release GitHub v1.0.0.

Préparation de la V1.0 — dépôt, vitrine et paquet (2026-08-08)

Demande de l'utilisateur : un dépôt propre et un README exhaustif avant la mise en ligne. Le tour d'inventaire a surtout montré que le dépôt l'était déjà — aucun fichier source mort, aucun résidu non suivi, et un paquet npm qui ne publiait que dist/, README.md et LICENSE, comme le plugin frère.

DateHashRésumé
2026-08-085922cfa package.json à 1.0.0. Chaîne de publication revérifiée avant d'en dépendre : build:prod compile, aucune source-map ne subsiste dans dist/, paquet à 4,7 Mo compressés / 18,9 Mo décompressés, 102 fichiers, dont un bundle principal à 0,45 Mo — le poids vit dans les deux chunks d'icônes, chargés à la première recherche seulement.
2026-08-085077d32 README réécrit : inventaire complet des fonctionnalités (logos dashboard-icons et variantes, sous-menus, snippets et leur héritage, notes, partage de dossier, mémoire de coupure des tunnels, déplacement SFTP, liens symboliques), section Known limitations assumant quatre écarts, et référence complète des réglages repliée sous Configuration — chaque valeur par défaut vérifiée par script contre configProvider.ts. Une seule phrase sur la désactivation des blocs, avec son bénéfice concret (alléger la sidebar), sur consigne explicite de l'utilisateur : un premier jet le répétait sur chaque fonctionnalité, ce qui était redondant.
2026-08-0883d9d2f Pied de page : « better-sidebar » ouvre le dépôt, « TooMuhtsh » le profil GitHub. Passage obligé par platform.openExternal() — un href nu aurait fait naviguer la fenêtre Electron elle-même, emportant toutes les sessions ouvertes. En-tête de la page de réglages aligné sur celui de Better Vault (titre + ligne secondaire au-dessus des sous-onglets, mêmes valeurs de style), pour que les deux plugins d'un même panneau s'ouvrent pareil.
2026-08-0883d9d2f Dépôt allégé, sur le modèle du frère : CLAUDE.md et .htmlvalidate.json retirés du suivi Git (git rm --cached + .gitignore) — outils de poste, sans intérêt pour qui clone. Ils restent en local et font toujours foi ; le validateur reste vert. Dix topics GitHub ajoutés. Conséquence à connaître : plus rien dans un clone ne pointe automatiquement vers .AIRules/, qui reste public et actif.

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

Trouvé en instruisant l'idée « Team Sharing » d'un backlog post-V1.0, et sans rapport avec elle : le défaut était dans le chemin déjà livré et publié du collage de dossier. Corrigé par un agent en worktree isolé, puis relu et revalidé. Fiche au registre du réalisé.

DateHashRésumé
2026-08-088bef7e9 Validé par l'utilisateur, fusion sur master. Le rejet total des profils local est sa décision explicite, l'alternative « les laisser voyager vidés de leur exécution » ayant été proposée et écartée.
2026-08-08cddbbc0 Trois trous : l'icône n'était pas sanitisée (asString() au lieu de DOMPurify) alors qu'elle est rendue par innerHTML brut dans un renderer où Node est disponible ; proxyCommand survivait au niveau secrets alors qu'il lance un processus local ; les options d'exécution d'un profil local n'étaient purgées à aucun niveau. Correctif de fond : passage d'une liste noire à une liste blanche d'options par type de profil, sanitiseIcon() extrait dans src/iconSanitize.ts et partagé avec workspaceShare.ts — l'asymétrie entre les deux fichiers était le signal. Banc de 13 charges hostiles, 13 passées, plus la rétrocompatibilité d'un export antérieur ; format inchangé, trois décomptes additifs.

Popups à formulaire en modales centrées (2026-08-08)

Dernier chantier ouvert de la roadmap, mené par un agent en worktree isolé puis relu et revalidé. Les deux popups qui portaient un formulaire — tunnels du profil, sélecteur d'icônes — passent sur le patron des modales existantes. Les menus contextuels restent des menus, comme la fiche l'exigeait. Fiche au registre du réalisé.

DateHashRésumé
2026-08-085dd826e Validé visuellement par l'utilisateur, fusion sur master.
2026-08-08634496b TunnelsModalComponent et IconPickerModalComponent créés ; le menu de bascule favori du picker migre dans la modale avec son propre gestionnaire de clic ; formatTunnel()/tunnelKey() sortis dans src/tunnels.ts. Le résultat se mesure au nettoyage : sidebarTree.component.* perd 758 lignes nettes — 26 méthodes, une douzaine de champs, deux entrées closest() du HostListener et deux blocs SCSS. Aucune clé i18n ajoutée ni retirée. Chargement différé des collections d'icônes préservé.

Menus contextuels en sous-menus nommés (2026-08-08)

Le menu du dossier portait treize entrées — « il s'étend vers l'infini », signalé par l'utilisateur le 2026-08-03. La fiche imposait de poser le critère de partage plutôt que de le deviner ; bien lui en a pris : aucun des deux angles qu'elle opposait (fréquence d'usage / nature du geste) n'a été retenu. L'utilisateur a demandé des sous-menus nommés à la façon de Windows. Chantier mené par un agent (Sonnet) en worktree isolé, puis relu et revalidé. Fiche au registre du réalisé.

DateHashRésumé
2026-08-086cd6f7d Validé visuellement par l'utilisateur, fusion sur master.
2026-08-08145d4eb Deux sous-menus latéraux — Manage (renommer, icône, note, favori) et More (snippets, copies, masquage ; côté profil : snippets, tunnels, masquage) — appliqués aux deux menus dans le même geste, comme la fiche le recommandait. Au premier niveau ne restent que les gestes fréquents, Supprimer isolé en bas. Panneau monté en frère du menu (piège #38), .group-context-submenu ajouté aux deux tests closest() du HostListener (piège #15 — sans quoi ouvrir un sous-menu refermait tout), bascule à gauche près du bord et recadrage par le clampInViewport() unique du plugin. Ouverture au survol comme au clic, fermeture différée de 180 ms pour le trajet diagonal. Clés Manage/More en fr/es/de.

Icônes dashboard-icons — troisième source du picker (2026-08-08)

Demandé par l'utilisateur : les collections en place offraient peu d'icônes reflétant des solutions IT. Chantier mené par un agent (Sonnet) dans un worktree isolé, puis relu, corrigé et revalidé. Design tranché en amont — ajout et non remplacement, tout embarqué hors-ligne plafonné à 50 Ko par fichier, variantes proposées par icône. Fiche complète au registre du réalisé.

DateHashRésumé
2026-08-08f4ebf8f Validé visuellement par l'utilisateur. Ajout de la version de Tabby de référence — 1.0.235 — au README et à la tête de la section environnement du contexte : elle n'était mentionnée nulle part, alors que toute la checklist de montée de version en dépend. Le tabby-core embarqué s'annonce en 1.0.231-nightly.0, numéro distinct que cible la peer dependency — les confondre fait chercher une incompatibilité inexistante.
2026-08-08ea02170 Trois isolations à la génération, dont une signalée en test : les logos devenaient des silhouettes blanches (le CSS de l'hôte bat les attributs de présentation d'un SVG injecté par innerHTML ; 2 716 fichiers sur 3 206 n'ont pas de fill sur leur racine). Trouvés en vérifiant : les id internes se contaminaient d'une icône à l'autre (id="a" dans 370 fichiers) et les <style> embarqués (578 fichiers, sélecteurs nus compris) fuyaient sur toute l'interface de Tabby. Corrigé une fois au vendoring, pas à l'exécution. Dans le même geste, le picker — position: fixed sans borne — couvrait la sidebar entière sur fenêtre courte, pied de page compris : borné et défilant, et clampInViewport() réserve désormais la bande du pied de page pour tous les popups.
2026-08-0889838b0, 93bab1c Script de vendoring (clone shallow, plafond 50 Ko, PROVENANCE.md à commit amont épinglé) et JSON embarqué ; troisième source de la recherche unifiée, chunk séparé chargé à la première recherche ; variantes en pastilles au clic gauche, le clic droit restant l'épinglage en favori ; entrée Apache-2.0 avec disclaimer de marques dans THIRD-PARTY-NOTICES.md.

i18n lot 4 — modules purs, clôture de la couverture (2026-08-08)

Dernier lot de l'internationalisation, mené par un agent (Sonnet) dans un worktree isolé sur consigne de l'utilisateur, puis relu et revalidé. Les modules purs sans accès à l'injecteur rendent un TranslatableMessage (src/i18nMessage.ts) — clé anglaise + paramètres — traduit au point d'affichage par sidebarTree.component.ts, seul appelant. Le chantier i18n est terminal : fiche basculée au registre du réalisé.

DateHashRésumé
2026-08-083b8b3f4 Fusion directe sur décision de l'utilisateur, sans passe visuelle — chemins d'erreur froids, aucun template touché, risque JIT nul ; lint:i18n (414 clés, complet) et build revalidés hors agent. Bascule de la fiche au registre dans le même geste.
2026-08-089810173 24 clés : erreurs de parsePayload()/parseWorkspacePayload()/sanitizeSvgIcon(), clauses de describePurge() en pluriels ICU, PROFILE_MODAL_UNAVAILABLE. describePurge() rend désormais la liste des clauses, jointes et traduites côté composant. L'extraction de lint-i18n.js reconnaît le motif message: '…' — générique pour tout futur TranslatableMessage.

Publication npm v0.4.0 (2026-08-08)

Publication demandée par l'utilisateur dans la foulée de deux validations du jour. Ce que la 0.4.0 apporte depuis la 0.3.0 : couverture i18n complète de la sidebar (lot 3 — arbre des profils, bloc Transferts, onglet de réglages, fr/es/de) et pied de page inviolable et opaque. Le lot 4 (messages d'erreur des modules purs) est en cours sur sa branche et partira dans une version ultérieure.

DateHashRésumé
2026-08-08df923a1 package.json à 0.4.0, npm publish (build build:prod via prepublishOnly), release GitHub v0.4.0.

Pied de page inviolable — colonne flex, contenu clippé, fond opaque (2026-08-08)

Signalé par l'utilisateur pendant la passe i18n : une ligne de transfert chevauchait le pied de page (fenêtre courte + fond translucide). La réserve par padding-bottom sous un pied de page absolu était une convention géométrique que tout débordement contournait. Exigence posée : rien ne doit entrer dans la zone, et aucune transparence, sur les deux vues. Branche fix/pied-de-page-intouchable.

DateHashRésumé
2026-08-08a389e63 Validé visuellement par l'utilisateur (les deux vues, fenêtre courte, non-régression menus/popups/glissoir), fusion sur master.
2026-08-08befba34 Le tag passe en colonne flex de deux membres de flux : une zone de contenu (flex: 1 1 auto; min-height: 0; overflow: hidden) qui clippe ce qu'elle ne peut pas afficher, et le pied de page (flex: 0 0 auto), plus jamais recouvert par construction. Fond rendu opaque quel que soit le thème : base pleine sous la teinte --theme-bg-more-2 (un thème vibrancy la rend translucide). Les popups (fixed) et le glissoir (absolute ancré au tag) restent hors flux et hors clip — vérifié sélecteur par sélecteur.

i18n lot 3 — sidebarTree, Transferts et onglet de réglages traduits (2026-08-08)

Dernier gros lot de l'internationalisation (issue GitHub #1) : les trois surfaces restées en français statique après les lots 1-2 passent aux clés anglaises + tables fr/es/de. L'onglet de réglages, cassé au lot 2 par le JIT (apostrophe dans un littéral inline), est refait selon le motif imposé par la fiche : tout en propriétés lbl*, aucun littéral inline. Branche feat/i18n-lot-3.

DateHashRésumé
2026-08-084a9c1b7 Passe visuelle utilisateur 12/12 (support de passe dédié, version testée dbdcf21) : JIT sain sur les trois surfaces, es/de vérifiés, langue non couverte retombe sur l'anglais, retour français sans écart. Fusion sur master. Reste un lot 4 mineur consigné dans la fiche : messages d'erreur de groupShare.ts, workspaceShare.ts, svgSanitizer.ts, profileModal.ts.
2026-08-08dbdcf21 sidebarTree (81 clés template + 51 côté TypeScript : menus, notices, tooltips, tunnels, uptime en méthodes d'instance) et onglet de réglages (83 clés en getters lbl*) convertis. Tables à 390 clés, complètes dans les trois langues ; trois collisions de msgid Tabby esquivées par reformulation (Recently launched, Modify, Erase) ; lint:i18n et build verts.
2026-08-08ee07b54 Bloc Transferts converti : template, confirmations d'annulation, badge et infobulle du registre en pluriels ICU, Session : {label} dans l'infobulle des lignes.

Remise à niveau 20260804-071239 : deux options tranchées, registre du réalisé créé (2026-08-08)

Les branches de sync du 2026-08-04, créées avant la fusion de la phase 0.3.0, étaient périmées — les fusionner aurait annulé la phase. Elles ont été supprimées (avec feat/phase-0.3.0, déjà fusionnée) et la propagation refaite depuis master, sur la branche governance-sync-20260804-071239.

DateHashRésumé
2026-08-08054a95d Charte 20260804-071239 propagée verbatim (empreintes Git vérifiées). Deux options ajoutées par la charte, tranchées : registre-livrés = oui, journal-format = tableau (motifs dans PROFIL.md). Application immédiate : bascule initiale de 33 chantiers livrés vers annexes/REALISE.html (fiches reprises verbatim + ligne de métadonnées journal/pièges/commits par entrée), roadmap de 3 384 à environ 730 lignes, une ligne de renvoi par chantier à ancre conservée — les liens existants aboutissent toujours. Fiche Écartée « Éditeur par type » déplacée en « Hors périmètre ». Validateur lint:airules vert.

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)

Livré le 2026-08-08 — trois groupes d'un coup, roadmap figée levée

Branche feat/phase-0.3.0, fusion finale de la phase à roadmap figée ouverte le 2026-08-03. G1 (2026-08-03) et G5 (2026-08-07) étaient déjà clos ; ce geste solde G2 (sections hautes de la sidebar), G3 (workspaces), G4 (liste SFTP), plus un socle d'internationalisation et la refonte du pied de page. À la consignation, le fichier currentwork_roadmap.html et les cinq plans de test HTML jetables sont supprimés, et la section « roadmap figée » retirée de CLAUDE.md : un régime dérogatoire ne survit pas à la phase qu'il servait. Le détail de chaque design vit dans les fiches de la roadmap, qui font foi.

Le fait de méthode du groupe : une passe de test de trois vagues, seize contrôles pilotés par l'assistant via CDP (injecteur Angular racine, mutations sous zone.run(), jamais un clic simulé), sur des données jetables (ZZZ … / grp-zzz-test-*) et avec sauvegarde de config.yaml vérifiée à l'octet avant/après. Workspaces 20/20, sessions et gel de vue et retour-auto et récents 16/16, SFTP (multi-sélection distante fichiers et dossiers, chargement par blocs, suppression et déplacement en masse) et transferts au complet. Une seule case n'a pas pu être pilotée et a été déléguée à l'œil de l'utilisateur : la confirmation d'annulation d'un transfert en cours — le composant des transferts est monté par Angular en mode production, donc inatteignable par CDP (window.ng n'expose que ɵcompilerFacade, __ngContext__ n'est qu'un entier vers un registre injoignable). Le mécanisme lui-même était déjà prouvé par ailleurs via ConfirmModalComponent pendant la passe SFTP.

Un faux négatif à ne pas rejouer : le « gel de la vue » a d'abord semblé cassé. Le diagnostic passait par launchProfile, dont le chemin composant réenveloppe les onglets ; capturé ainsi, l'onglet lié paraissait dériver. Un diagnostic direct (expr-diag-freeze.js) a montré que l'onglet lié restait bien épinglé et frozen=true — l'artefact venait de l'instrument, pas du code. Régle de la maison, reconfirmée : lire l'état écrit, pas l'écran, et se méfier de l'outil de mesure avant de conclure au défaut.

Le sélecteur de workspaces a été repris deux fois sur retour utilisateur, et c'est la deuxième reprise qui a trouvé la bonne forme. Le mode « liste » n'est plus une valeur de contextMenuMode mais un état propre (workspaceSwitcherOpen) : la liste déroulante reste affichée sous le menu de gestion et ses sous-popups, qui s'ouvrent désormais à un ancrage fixe au bord droit de la liste (switcherPopupAnchor()), jamais au curseur — un clic dans la liste ne fait plus disparaître le panneau, ce qui rendait l'usage pénible. Le choix onglets/liste est un réglage explicite (workspaceSelectorMode), la bascule automatique par ResizeObserver ayant été refusée au test. Le clamp de position exclut .workspace-switcher-menu, puisque deux popups coexistent maintenant par conception.

Le pied de page réserve enfin sa bande. L'engrenage, les lignes de transfert SFTP et le cadre natif se chevauchaient en bas de sidebar. Le correctif n'est pas un simple rembourrage : une barre de pied fixe occupe une bande réservée (padding-bottom du conteneur augmenté d'autant), l'engrenage y est agrandi et déplacé dans le flux, et la ligne libérée affiche better-sidebar by TooMuhtsh v{{version}} à gauche — la version est importée de package.json (resolveJsonModule). Autre réglage tranché par l'utilisateur : « Profils récents » (showRecentProfiles) est désormais désactivé par défaut, une meilleure présentation étant remise à un prochain lot.

i18n : un socle posé, partiel — et un incident qui n'apparaît qu'à l'exécution

L'internationalisation est greffée sur le ngx-translate de Tabby : la chaîne source anglaise est la clé, et setTranslation(lang, table, true) — troisième argument shouldMerge impératif — ajoute la table sans effacer les traductions de Tabby. @ngx-translate/core reste external (fourni par tabby-core). Trois tables (fr-FR, es-ES, de-DE), l'anglais étant la clé elle-même ; un validateur npm run lint:i18n refuse toute clé manquante, morte, à paramètre absent, à ICU cassé ou qui écraserait une valeur de Tabby. Couverture actuelle : partielle — zone SFTP (lot 1) et modales (lot 2) faites ; sidebarTree, transfers et l'onglet de réglages restent à traduire (lot 3 à venir).

L'incident du lot 2, à ne jamais rejouer : lier une chaîne anglaise à apostrophe droite dans une expression Angular inline ({{ '…' | translate }}, [title]="'…' | translate") casse la compilation JIT au runtime — l'apostrophe termine le littéral de chaîne. npm run build (webpack/tsc) ne le voit jamais : il ne fait pas tourner le JIT d'Angular. L'onglet de réglages, cassé ainsi, a été restauré en français statique (attributs title="…" statiques, hors i18n) et ses descriptions raccourcies à une phrase brève chacune ; 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 été remplacés par : par prudence (les valeurs FR/ES/DE gardent leur tiret, elles vivent dans les .ts et ne passent jamais par le JIT).

G3 a été construit par agents séquentiels sur la branche de phase, chaque passe build-vert, puis repris à la main sur les deux retours utilisateur du sélecteur. Trois écarts de design à retenir, détaillés dans la fiche workspaces : la ligne d'insertion du réordonnancement n'est pas livrée (placeholder CDK verrouillé à hauteur nulle contre l'oscillation, pièges #25/#28) ; la couleur contextuelle passe par le champ color natif des profils (icône teintée + barre sous l'onglet, palette TAB_COLORS), injecté par structuredClone au lancement, la couleur propre d'un profil gagnant toujours ; l'export/import de workspace vit dans src/workspaceShare.ts (ids inconnus conservés, piège #74, icône SVG resanitisée à l'import).

DateHashRésumé
2026-08-08d284e62 G2 : historique des profils récents (MRU, désactivé par défaut) · flèches d'activité ↑/↓ sur le profil · états de transfert sur la ligne de session · confirmation avant annulation d'un transfert. G3 : icône par workspace · export/import JSON (workspaceShare.ts) · sélecteur onglets/liste (état propre, ancrage fixe) · couleurs contextuelles (champ color natif) · retour visuel réordonner/imbriquer · engrenage en pied de sidebar. G4 : gel de la vue SFTP · retour auto sur Profils (activé par défaut) · multi-sélection distante fichiers et dossiers · chargement par blocs. Socle i18n (fr/es/de, greffé sur ngx-translate, couverture partielle SFTP + modales, validateur lint:i18n). Pied de page réservé + marque + version. Passe de test : 3 vagues, 16 contrôles CDP + visuels, une case déléguée à l'utilisateur.

G5 — transferts fiables et tunnels à état de reprise (2026-08-07)

Livré et validé par l'utilisateur le 2026-08-07 — passe 24/24

Branche feat/phase-0.3.0, deuxième groupe de la phase à roadmap figée. Quatre items écrits d'affilée et commités d'un seul geste. Détail de chaque design dans les fiches de la roadmap, qui font foi. Première passe majoritairement pilotée par l'assistant : 16 tests sur 24 exécutés par le harnais CDP (injecteur Angular, jamais de clic simulé), 8 à l'œil de l'utilisateur.

Le fait marquant du groupe : les deux vrais défauts de la passe ont été trouvés par les contrôles eux-mêmes, dès leur première exécution. Le contrôle d'arrivée a déclaré « incomplet » un lien symbolique parfaitement livré — la taille d'un lien dans un listing est celle du lien, le téléchargement livre la cible ; d'où l'extraction du résolveur borné à 8 sauts dans remoteEntry.ts, partagé entre l'édition distante et la vérification. Et la cause réelle des tunnels affichés en 3-4 exemplaires n'était pas celle supposée : pas de doubles listeners, mais le multiplexage — plusieurs onglets partagent la même SSHSession et le panneau listait le même tableau une fois par onglet. Sur ce poste, un second bind local est refusé (EADDRINUSE, mesuré y compris à chaud) : le démontage automatique de doublon reste un filet de sécurité au chemin non exercé, à ne pas présenter comme validé.

Un fait établi sur les sources installées, qui a dicté le design des tunnels : il n'existe aucune reprise native à refléter. La micro-coupure détruit la session entière ; à la reconnexion de l'onglet, seuls les tunnels du profil sont remontés, et un tunnel ajouté à la volée disparaît en silence. Les trois états du panneau (« reprise… » 60 s, retour à live, « non remonté » 30 s) sont la mémoire que Tabby n'a pas. Vérifié au test 20 et voulu : fermer un onglet d'une session multiplexée laisse le tunnel vivant tant qu'un autre onglet la référence.

Méthode de passe, réutilisable : les services du plugin ne sont pas exportés par son paquet — ils se retrouvent par leur nom de classe dans inj.records de l'injecteur racine (complément au piège #8). pickDirectory() neutralisé vers un banc local (un dialogue natif fige le renderer, CDP compris), profil SSH jetable zzz-test-g5-tunnel créé puis retiré avec contrôle d'intégrité de la config à l'octet près, banc distant /tmp/zzz-test-g5 nettoyé, ForwardedPort fabriqué par prototype d'une instance vivante pour le montage à chaud. Harnais dans le scratchpad de la session.

Ouvert par l'utilisateur pendant la passe, consigné sans être tranché : le dialogue « Enregistrer sous » d'un téléchargement de fichier par menu contextuel affiche « 127.0.0.1 » — l'hôte de notre offre HTTP locale, non remplaçable par un texte arbitraire. Options dans la fiche panneau d'état.

DateHashRésumé
2026-08-07ccb586b Les quatre items de G5 : « Download directory » sur la voie du plugin (pickDirectory + arbre parallélisé, une ligne par fichier, collision « (2) », « interrompu à N % » sur coupure — vérifié à 23 %) ; contrôle d'arrivée sur toute voie à chemin imposé (état unsound « incomplet à destination », symlinks résolus) ; tunnels à mémoire de coupure (« reprise… » / « non remonté ») + dédoublonnage multiplexage + demande utilisateur du jour ; chemin distant + session en infobulle des lignes. Passe 24/24 : 16 tests pilotés CDP sur serveur réel, 8 visuels.

G1 — duplication, snippets, notes et partage de groupe (2026-08-03)

Livré et validé par l'utilisateur le 2026-08-03

Branche feat/phase-0.3.0, premier groupe de la phase à roadmap figée. Cinq items écrits d'affilée et commités d'un seul geste — c'est le régime, pas un oubli. Détail de chaque design dans les fiches de la roadmap, qui font foi.

Le fait marquant du groupe n'est aucun des cinq items : c'est que trois d'entre eux se sont révélés autres que leur fiche l'annonçait. Quick Snippets décrivait des listes de commandes par profil et est devenu une bibliothèque partagée avec variables. Group Exec n'était pas un item neuf — la fonction existait, seuls ses enrichissements restaient, et ils ont été refusés. Partage de groupe reposait sur une idée fausse du coffre-fort. Chaque fois, ce qui a changé le périmètre est le même geste : lire le code, ou poser la question, avant de chiffrer. Une fiche de roadmap consigne une intention écrite tôt, pas un état.

Deux défauts sérieux ont été trouvés par la mesure, pas par la relecture, et les deux méritent d'être retenus pour la méthode plus que pour le cas.

La purge, étant du code pur, a été vérifiée hors de Tabby avant toute passe manuelle : 59 contrôles sur src/groupShare.ts, refus et entrées malformées compris. C'est ce banc qui a trouvé le premier des deux défauts. La passe réelle a suivi, 36 points, et son contrôle le plus fort porte sur le résultat écrit et non sur l'export : vingt profils collés, aucun ne portant de secret, les quatre seules entrées encore chargées étant les originaux du banc.

Un doute de test qui n'en était pas un

La hiérarchie des dossiers collés a été rapportée comme suspecte — « les dossiers se sont mis à la racine ». Vérification faite dans la configuration : chaque dossier collé portait bien son sous-dossier. Ce qui avait changé venait d'ailleurs — les trois dossiers du banc avaient un uuid à la place de l'id posé à la main, et un dossier jamais collé s'était retrouvé sous un autre. Or seul le re-parentage change un id (#62) et le collage ne touche jamais un dossier existant : c'était un rangement manuel au glisser-déposer. Un id qui change nomme le geste qui l'a changé — c'est ce qui a permis de trancher sans reproduire.

Deux pièges d'environnement consignés au passage, tous deux sortis de la préparation du banc de test : #76 — un commentaire écrit dans config.yaml ne survit pas au premier config.save(), Tabby réécrivant tout depuis son état mémoire, ce qui rend inopérant tout retrait fondé sur des marqueurs — et #78, le fonctionnement réel du coffre-fort.

Un chantier ouvert par l'utilisateur pendant la passe, consigné sans être traité : le menu contextuel du dossier porte désormais treize entrées, dont les deux dernières viennent de ce chantier.

DateHashRésumé
2026-08-03e2cea77 Les cinq items de G1 : Clone Express, Quick Snippets (bibliothèque partagée, variables, comportement par couple élément/snippet), Notes de profil, Group Exec clos sans code, et Partage de groupe (deux niveaux de purge, collage avec arbitrage de collision, purge rejouée à l'entrée). Plus les interrupteurs showSnippets et showNotes, qui coupent la source et pas seulement la vue. 59 contrôles hors Tabby + 96 points de test manuel sur cinq passes et une passe finale.

Régime temporaire : roadmap figée, consignation en une fois (2026-08-03)

Décidé par l'utilisateur le 2026-08-03

Ce n'est pas un chantier mais un mode de travail, noté ici parce qu'il explique une anomalie autrement déroutante à la relecture : l'absence de commits et d'entrées de journal pendant la période qui suit.

La roadmap venait d'être remise à jour et il ne reste que des items petits, indépendants et sans couplage à trancher. Consigner chacun coûtait alors plus cher que le geste lui-même. D'où la décision : ce qui est demandé et réalisable de suite se fait de suite, sans ouvrir de fiche, et tout est consigné en une seule fois à la fin.

Ce qui est suspendu est la consignation au fil de l'eau, pas la prudence : l'option branches reste en vigueur et c'est elle qui rend le reste possible — le travail vit sur une branche unique, si bien que master ne porte à aucun moment du code que la gouvernance ne décrit pas, et la fusion finale amène le code et sa documentation d'un seul geste. Aucune option du PROFIL.md ne change : une modification d'option relèverait de l'entretien de cadrage, pas d'une organisation de travail.

L'état vit dans .AIRules/currentwork_roadmap.html — 24 items restants en six groupes, avec difficulté, jauge d'utilité et état d'avancement, plus un journal de phase qui sert de matière à la consignation. Ce fichier est supprimé à la consignation finale, en même temps que la section correspondante de CLAUDE.md : le laisser vivre créerait un second endroit où lire le même fait, et un régime dérogatoire qui survit à sa raison devient une règle par simple oubli.

Les points de veille restants : deux appuis retirés, le dernier point rouge rendu bruyant (2026-08-03)

Livré, testé et validé le 2026-08-03

Branche refactor/points-de-veille. Session dédiée demandée par l'utilisateur, sur la fiche #points-fragiles ouverte le matin même.

La piste que la fiche recommandait aurait fabriqué une fausse panne. Elle proposait de lire ɵcmp.inputs pour vérifier que la modale de profil déclare toujours les deux entrées que le plugin lui assigne, en demandant d'établir d'abord « la forme exacte de la table ». Bien lui en a pris : cette table est indexée par le nom public de l'entrée, et la modale déclare @Input('profile') partialProfile. Une précondition y cherchant la clé partialProfile aurait échoué à tous les démarrages, sur un Tabby parfaitement sain — un contrôle ajouté pour rendre une panne visible aurait produit une panne imaginaire, ce qui est pire que le silence qu'il remplaçait.

Deux corrections en sont sorties, consignées au piège #75. reflectComponentType() est de l'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 » — tombait d'elle-même, il n'y avait pas lieu de lire un champ interne. Et son résultat se lit par propName, jamais templateName, contrairement à l'exemple donné par la documentation d'Angular, qui intervertit les deux : c'est toRefArray() dans le bundle réellement chargé qui a tranché, pas le commentaire au-dessus.

fragile-7, le seul point rouge, dans l'ordre que ce projet s'impose — réduire d'abord, surveiller ensuite. src/profileModal.ts devient le point unique par lequel la modale s'ouvre : les deux appelants la nommaient chacun, elle n'est plus nommée qu'une fois, et c'est cette liste-là que la précondition edit-profile-modal confronte à ce qu'Angular déclare. Un satisfies l'arrime à l'augmentation de types — ce qui ne prouve rien sur Tabby, mais interdit que la liste vérifiée et la liste assignée divergent. En cas d'écart : un message nommant le champ absent et les entrées réellement déclarées, une notice, et un refus d'ouvrir plutôt qu'une fenêtre vide dont personne ne peut rien tirer.

fragile-5 : la discipline devient structure, et une supposition devient une mesure. readProfileGroups() est désormais le seul appel du plugin à getProfileGroups() et rend un clone par construction ; les deux appelants recopiaient les mêmes lignes et le même avertissement, et rien n'obligeait un troisième à le faire. La mesure a rapporté davantage : 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 reste malgré tout : les bretelles sont un détail d'implémentation d'une méthode que personne n'exporte, et ce qu'elles retiennent est un config.yaml corrompu. Une sonde vérifie cette garantie une fois par session au lieu de la supposer.

fragile-8 : deux appuis retirés plutôt que documentés. La surcharge passe de downloadItem() à download(), où les deux voies aboutissent : la question « l'entrée de menu appelle-t-elle bien downloadItem() ? » devient sans objet, et cette méthode redevient celle de Tabby. Et l'absence d'un gestionnaire will-download, jusque-là supposée, est demandée avant chaque téléchargement par listenerCount() — méthode d'EventEmitter, donc réellement contractuelle, ce que rien d'autre n'est dans ce voisinage ; s'il y en a un, la voie héritée reprend la main.

Ce que ce chantier n'a pas fait

Il ne rend le plugin compatible avec rien. 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. Deux appuis ont réellement disparu, et c'est écrit comme tel ; les autres points ont seulement appris à parler. Un tableau où plus rien n'est rouge se relit trop facilement comme un tableau de points supprimés.

Passe de test du 2026-08-03 — pilotée par l'assistant, à la demande de l'utilisateur

Le contrôle a été vérifié dans les deux sens, faute de quoi il n'aurait rien prouvé. Dans le bon : démarrage silencieux, modale s'ouvrant en portant le profil qu'on lui passe — donc l'entrée est bien assignée et consommée —, configuration inchangée par empreinte, arbre chargé sans qu'aucun groupe vivant ne gagne de propriété calculée. Dans l'autre : avec un nom d'entrée volontairement faux, recompilé pour l'occasion puis restauré, le démarrage sort le message attendu avec la liste des entrées réelles, la notice s'affiche, l'ouverture est refusée — pendant que la sidebar et son arbre restent intacts, la précondition n'étant pas fatale.

Passe SFTP sur le serveur VPN, dans /tmp/zzz-test-dl/ créé puis supprimé, sur un fichier au contenu déterministe (seq 1 20000, 108 894 octets — taille recalculée à la main pour que le banc lui-même soit vérifiable). Le chemin part de downloadItem(), c'est-à-dire de 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 : un lien symbolique reste sur la voie héritée, un will-download installé pour l'occasion y renvoie aussi, son retrait rend la main à la voie HTTP.

Deux points de méthode. Aucune boîte de dialogue 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). Et le panneau SFTP ne se construit pas tant que la sidebar est sur la vue Profils : c'est setSftpMode(true) qu'il faut appeler, l'entrée active du composant étant précisément ce qui l'empêche d'ouvrir un canal pour une vue que personne ne regarde.

DateHashRésumé
2026-08-030d70d05 Points de veille : deux appuis retirés, le point rouge rendu bruyant. src/profileModal.ts et src/profileGroups.ts comme points uniques, précondition edit-profile-modal étendue aux deux entrées par reflectComponentType(), surcharge SFTP déplacée sur download(), sonde will-download. Piège #75.

Chaque bloc de la sidebar s'éteint, et son extinction coupe la source (2026-08-03)

Livré et validé par l'utilisateur le 2026-08-03

Branches feat/interrupteurs-par-bloc puis feat/transferts-sous-sftp. Proposé par l'utilisateur : garder la puissance du « tout-en-un » sans l'imposer.

Le chantier n'était pas le *ngIf. Six clés sous sidebarPlus, toutes à true par défaut, mais éteindre coupe ce qui alimente le bloc : le balayage de tous les onglets de tous les splits toutes les 2 s cesse de s'exécuter, la sonde de latence reçoit une liste vide et n'interroge plus aucun serveur, track() refuse toute inscription en un point unique qui couvre fileTransferStarted$ comme nos propres appels. Un interrupteur qui n'aurait masqué que la vue aurait épuré l'écran en gardant le coût.

Le composant SFTP est détruit et non caché — l'inverse du choix fait pour la bascule de mode, qui reste en [hidden] pour préserver son cache de panneaux. Les deux ont raison parce que ce ne sont pas les mêmes questions : changer de mode est temporaire, éteindre le bloc est censé rendre les canaux.

Un garde-fou demandé par l'utilisateur a trouvé un vrai défaut, et c'est le test écrit pour le vérifier qui l'a attrapé, pas la relecture. Éteindre les workspaces fait retomber l'affichage sur « Tous », et loadTreeItems() persistait cette valeur dans localStorage en fin de course : le workspace réellement choisi était perdu, et rallumer ne le rendait pas. D'où la règle, désormais écrite : 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 de toute la configuration — profils, groupes, workspaces avec masquages, favoris et ordres, tunnels configurés d'un profil — bit à bit identique de part et d'autre d'un cycle complet.

Le gestionnaire de transferts est passé sous la vue SFTP en seconde passe, à la demande de l'utilisateur. Le corollaire n'était pas dans la demande et compte autant : le registre reflète tous les transferts de l'application, y compris ceux du panneau natif et des autres plugins. Les rendre invisibles pendant que hideNativeTransfersMenu reste coché laissait l'utilisateur sans aucun affichage. Le masquage du menu natif est donc suspendu dès que notre panneau ne s'affiche plus — 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.

Page de réglages refondue dans le même geste, sur trois règles de l'utilisateur : deux onglets (« Général », « Fonctionnalités ») plutôt qu'un long déroulé ; chaque option sous l'interrupteur de la fonctionnalité dont elle dépend, et qui disparaît avec elle — un réglage qui survit à sa fonctionnalité se lit comme cassé, pas comme sans objet ; descriptions d'une à deux lignes, le détail passant en infobulle sur une icône (i). Les blocs sans réglage propre sont groupés et resserrés en tête. Ce qui règle du même coup l'agencement qu'attendait « Options & polissage UI ».

DateHashRésumé
2026-08-030696799 Le gestionnaire de transferts devient une option de la vue SFTP, et le masquage du menu natif de Tabby est suspendu dès que notre panneau ne s'affiche plus. Blocs sans réglage groupés en tête de page. 10 contrôles CDP.
2026-08-0312ad2b7 Six interrupteurs par bloc, coupant la source et pas seulement la vue ; reconcileHiddenBlocks() pour les retombées ; persistance du workspace actif préservée ; page de réglages en deux onglets. 35 contrôles CDP.

Le point le plus fragile du plugin a été supprimé, pas consolidé (2026-08-03)

Livré et validé par l'utilisateur le 2026-08-03

Branches refactor/edition-profil-modale-directe puis feat/controle-compatibilite-hote. Parti d'une observation de l'utilisateur sur la dette d'intégration du plugin.

L'inventaire a précédé le correctif, et il a changé la conclusion. Quatre natures de couplage à l'hôte, très inégalement risquées : le DOM non versionné de Tabby (le seul réellement muet), les classes empruntées à tabby-ssh (exportées — le risque est leur dédoublement, pas leur absence), le SCSS global (débordement, discipline déjà tenue) et les API publiques de tabby-core (contractuelles, rien à durcir). Distinguer les quatre a évité de dépenser le chantier sur ce qui ne risquait rien.

« Éditer… » pilotait le DOM de tabby-settings : ouvrir l'onglet Paramètres, déplier tous les dossiers par des clics successifs, retrouver la ligne en comparant le texte d'un span, la cliquer, puis sonder .modal-content toutes les 300 ms pour savoir quand refermer. Le commentaire qui portait ce code affirmait qu'aucune API publique n'existait — et le même fichier ouvrait déjà cette modale quelques centaines de lignes plus haut, pour « Nouveau profil… ». editProfile() reprend donc les huit lignes de tabby-settings. Environ 135 lignes retirées, et le point de veille fragile-1 avec.

La leçon, plus large que ce cas : le premier réflexe devant un couplage fragile avait été de mieux surveiller la casse. Retirer le couplage a coûté moins cher que le garde-fou qu'on s'apprêtait à construire. Avant d'instrumenter un point de contact : peut-il disparaître ? Sinon seulement, comment le rendre bruyant. Un contournement survit à la raison qui l'a fait naître, et son commentaire devient l'argument qui empêche de le rouvrir.

Ce qui reste couplé est désormais vérifié. Quatre préconditions nommées dans src/hostCompat.ts, évaluées une fois à app.ready$, chacune portant la fonctionnalité qu'elle fait perdre — seul le conteneur de montage est fatal. Pas de contrôle de version de Tabby : un numéro crierait au loup sur une version compatible et se tairait sur un build local qui a bougé. Et une sonde pour ce qu'aucun contrôle statique ne peut voir : avec deux tabby-ssh chargés, les quatre préconditions passent — les classes existent, ce sont les mauvaises. isSSHTab() porte cette sonde, et les trois instanceof dispersés y passent désormais : le partage ne gagne pas une ligne, il crée le point où l'hypothèse est vérifiée au lieu d'être supposée.

Ce que ce contrôle ne fait pas, à ne pas relire plus tard comme une promesse : il ne rend pas le plugin plus compatible et ne réduit ni ce qui casse ni la fréquence. Il change le mode d'échec, d'un non-événement vers quelque chose qui se lit.

DateHashRésumé
2026-08-033ecac9b Contrôle de compatibilité à l'hôte (src/hostCompat.ts) : quatre préconditions nommées à ready$, signalement unique en notification et en console, et sonde du piège #34 portée par isSSHTab(). 18 contrôles CDP, chemin fatal et restauration compris.
2026-08-03e386b94 editProfile() ouvre EditProfileModalComponent directement, comme tabby-settings pour sa propre liste. Suppression de clickNativeProfileRow(), watchForNativeModalClose(), modalWatchInterval et wait(). 15 contrôles CDP.

Un id supprimé ne laisse plus de trace derrière lui (2026-08-03)

Livré et validé par l'utilisateur le 2026-08-03

Branche fix/ids-morts. Dernier point ouvert du voisinage config.store, laissé de côté le 2026-08-02 comme « cosmétique, à traiter quand on y repassera ».

favorites, favoriteGroups, hiddenProfileIds et hiddenGroupIds gardaient l'id d'une entrée supprimée, dans chaque workspace, sans que rien ne les ramasse. Aucune perte de données — une accumulation qui ne se vide jamais.

Ce qui a décidé de la forme du correctif, et qui n'était pas prévu : la solution évidente était d'élargir pruneDeadOrderIds() aux quatre listes. Elle est fausse. Un balayage doit répondre « cet id existe-t-il encore ? », et config.store.profiles/.groups ne portent que les entrées de l'utilisateur : l'arbre affiche aussi les profils fournis par les providers et les groupes synthétiques qui les portent, absents de config.store. Le balayage aurait donc supprimé les favoris de ces profils-là — soit exactement l'inverse du but. Le nettoyage se fait au point de suppression, où l'id est connu d'emblée et où rien n'a à être déduit. Voir piège #74.

Un défaut latent est tombé au passage, absent de la fiche : pruneDeadOrderIds() jugeait déjà la vie d'un id sur config.store seul, donc la position d'un profil builtin comptait pour morte et retombait à son défaut au prochain réordonnancement. Il consulte désormais aussi rawGroupsSnapshot. C'est en cherchant pourquoi la solution évidente était dangereuse qu'il est apparu.

Ce que le re-parentage ne fait pas, et ne doit pas faire : il n'appelle pas ce nettoyage. migrateWorkspaceGroupId() a déjà renommé toutes les traces de l'ancien id avant que le groupe ne soit supprimé — migrer, puis supprimer. Inverser les deux, ou ajouter le nettoyage « par symétrie », effacerait ce qui vient d'être migré. Écrit dans le code à l'endroit même où la question se pose.

DateHashRésumé
2026-08-033884789 forgetDeletedId() appelée depuis deleteProfile() et deleteGroup(), sur tous les workspaces, l'état replié du dossier compris. pruneDeadOrderIds() consulte désormais l'instantané non filtré.

Le SCSS ne déborde plus sur Tabby, et les icônes ne sont plus chargées au démarrage (2026-08-03)

Livré et validé par l'utilisateur le 2026-08-03

Branche fix/scss-global-et-icones. Les deux derniers points de code du ménage avant release, qui clôt le chantier.

La fuite de style s'est révélée à la fois plus large et plus étroite que décrite. La fiche annonçait deux débordements et une classe impossible à cadrer ; il y en avait trois, dont deux se cadrent très bien, chacun d'une façon différente. Ce qui a permis de trancher, c'est d'avoir lu la source de @angular/cdk installée plutôt que de raisonner par analogie : le placeholder ne quitte pas la liste (parent.replaceChild()), l'aperçu si — et se nomme par cdkDragPreviewClass, l'entrée que CDK expose pour ce cas exact —, et .cdk-drag-animating, absente de la fiche, est du côté de l'aperçu : la cadrer par le tag ne l'aurait pas restreinte, elle l'aurait supprimée. Détail en piège #71.

Un commentaire du code affirmait le contraire, et se recopiait de proche en proche vers les règles voisines. C'est le genre d'erreur qui ne se voit pas à la relecture : elle décrit un comportement plausible et cohérent avec ce que fait l'aperçu juste à côté.

Le reste du fichier a été confronté aux builtin-plugins/ de l'app installée, classe par classe — la méthode qui avait fait trouver .grabber. Aucune autre n'y apparaît : elles restent globales en connaissance de cause, pas par omission.

Les icônes passent en chargement à la demande. Les deux collections Iconify sont 97 % du paquet et étaient importées en entier, donc décodées à chaque lancement de Tabby, pour un sélecteur que la plupart des sessions n'ouvrent jamais. Un import() dynamique les sort du bundle de démarrage — sans réduire le paquet publié, ce qui est voulu et inévitable : les @iconify-json/* sont des devDependencies, absentes d'une installation utilisateur, donc le chunk doit voyager avec. Ce qui tombe, c'est le coût de démarrage. Deux conditions non évidentes, consignées en #72 et #73.

DateHashRésumé
2026-08-03fb4b719 Collections Iconify en import() dynamique. Bundle chargé au démarrage : 5,33 → 0,26 Mo en production, 5,94 → 0,88 Mo en développement ; le chunk fait 5,07 Mo. chunkIds: 'named' forcé en production, sans quoi le nom du chunk change avec le mode et files: ["dist"] en publie deux.
2026-08-03551d241 Trois classes CDK et .grabber cessent de déborder sur Tabby. Vérifié dans les sélecteurs réellement émis par le bundle, pas dans le SCSS source.

Cadrage relu, et seuil passe à roadmap-libre (2026-08-03)

Entretien complet rouvert à la demande de l'utilisateur, sous la charte 20260731-204511. Les vingt-deux clés de la charte et les vingt-deux lignes de PROFIL.md ont été confrontées une à une : elles correspondent exactement, aucune ajoutée ni retirée. La ligne « Charte au moment du cadrage » passe donc de 20260731-150737 à 20260731-204511 — elle attendait précisément cette relecture, et l'avancer plus tôt aurait écrit un fait faux (voir le chantier de remise à niveau).

Une seule option change : seuil, de strict à roadmap-libre. La roadmap s'écrit désormais librement ; le journal reste sous validation. Le motif n'est pas une commodité mais un défaut constaté deux fois le même jour : un chantier corrige en passant ce qu'une autre fiche décrit — les trois défauts mineurs du voisinage config.store, corrigés en construisant l'intégrité des données, puis build:prod et filesize, réglés dans la préparation de la 0.3.0 — et la fiche voisine, qui n'appartient à aucun de ces chantiers, reste périmée faute d'un feu vert que personne ne pense à demander. Une roadmap périmée ment autant qu'une roadmap écrite sans arbitrage : c'était le risque inverse de celui que strict protège, et c'est celui-là qui s'est réalisé.

Contrepartie assumée, écrite ici pour qu'elle ne se redécouvre pas : un statut de chantier peut désormais bouger sans arbitrage explicite de l'utilisateur. Ce qui la borne, c'est que le journal, lui, ne bouge pas — et qu'un statut « Fait » continue de reposer sur une validation en conditions réelles, qui est une autre règle.

DateHashRésumé
2026-08-0350cfd89 seuilroadmap-libre, les vingt-et-une autres options confirmées telles quelles. CLAUDE.md mis à jour dans le même geste : il annonçait un feu vert portant sur la roadmap et le journal.

Le build de publication n'embarque plus de source-map (2026-08-03)

Premier des points de Ménage avant release soignée à être traité. Deux des trois y étaient déjà faits sans que la fiche le dise : build:prod existe et prepublishOnly l'appelle depuis le d9d291b (2026-08-02), et filesize y a rejoint les devDependencies dans le même commit. La fiche décrivait donc un état périmé de deux jours — même travers que le bloc des trois défauts mineurs : un chantier corrige en passant ce qu'une autre fiche décrit, et la fiche ne suit pas.

Ce qui restait, et que personne n'avait vu : --mode production est passé en ligne de commande, et webpack-cli le fusionne par-dessus la configuration. La clé mode était donc bien corrigée, mais le fichier de config, lui, continuait de se croire en développement : devtool: 'source-map' et pathinfo: true gardaient leur valeur. Le paquet publié était minifié et embarquait sa source-map. La configuration s'exporte désormais en fonction, seule forme qui voit argv.mode.

Pourquoi pas output.clean, qui aurait été la réponse évidente au fichier résiduel : les .d.ts que désigne typings sont écrits directement dans dist/ par TypeScript (declarationDir du tsconfig), et ne passent pas par les assets webpack — vérifié en construisant vers un autre --output-path, qui n'a produit qu'index.js. Les nettoyer aurait publié un paquet sans typings. Le script de build retire la source-map lui-même.

DateHashRésumé
2026-08-039cf21dc devtool et pathinfo suivent enfin le mode réel. dist/ de publication passe de 6,3 à 5,4 Mo, .d.ts compris. Aucun test dans Tabby : rien de ce commit n'atteint le build de développement, qui garde sa source-map.

Purge des données réelles de l'utilisateur dans le dépôt public (2026-08-03)

Demande de l'utilisateur, née d'une vérification d'attribution : la documentation de ce dépôt public citait le nom de l'employeur de l'utilisateur, un nom d'hôte de son infrastructure et l'organisation de son parc, repris de ses vraies données au fil des passes de test. Rien de secret — aucun identifiant, aucun accès — mais de l'information interne qui n'a pas à être publiée.

Ce que la vérification a établi au passage, et qui n'était pas l'objet de la demande : ce dépôt ne porte aucune métadonnée d'attribution IA, sur les 149 commits et toutes références confondues, et son unique contributeur GitHub est TooMuhtsh. La purge du 2026-08-01 a tenu et aucun commit depuis n'en a réintroduit. La page GitHub qui semblait dire le contraire servait un cache.

Exception assumée à l'ajout seul : la purge a réécrit les entrées existantes de ce journal, seule façon de retirer la mention de l'historique. Deux motifs de remplacement, App.<employeur>.fr vers app.exemple.fr et le nom seul vers Travail, appliqués à tout l'arbre de chacun des 149 commits — dont tabby_sidebar_roadmap.md, supprimé du HEAD depuis le 2026-07-26 mais toujours vivant dans l'historique. C'est le genre de fichier qu'une recherche au HEAD ne montre pas.

Deux enseignements sur la réécriture, qui corrigent le piège #11 : filter-branch efface sa table de correspondance en fin de course, il faut donc la reconstituer depuis refs/original — et l'apparier sur le sujet et la date de chaque commit, pas seulement sur la position, faute de quoi un décalage d'un cran passerait inaperçu. Et le contrôle « git diff refs/original… master doit être vide » ne vaut que pour une purge de messages : ici l'arbre change par construction.

Ce qui n'est pas refermé, et ne peut pas l'être depuis le poste : un push --force ne supprime rien chez GitHub. L'ancien sommet répond encore par son hash. Aucune référence ne pointe plus vers lui (ni fork, ni refs/pull/*, ni tag), donc un ramassage côté GitHub finira probablement par l'emporter, mais rien ne le garantit ni ne le date — seul un ticket au support purge réellement. Arbitrage de l'utilisateur : on laisse courir, le contenu retiré ne donnant aucun accès. Voir piège #70.

Le paquet npm publié (0.2.0, 2026-07-26) a été téléchargé et inspecté : indemne, la mention étant postérieure à sa publication.

DateHashRésumé
2026-08-037b3c461 Hashs cités remis à jour. 123 références dans le journal et la roadmap, la réécriture les ayant toutes périmées. Six chaînes hexadécimales laissées intactes à dessein : deux empreintes de blob de la charte (6e52454, 10ddd25, cf. #47) et deux ids de groupe Tabby — aucune n'est un commit.
2026-08-030192382 Réécriture des 149 commits (sommet d'origine dc8fd95). Sauvegarde préalable en clone miroir hors du dépôt, pas en branche — #11. refs/original et reflog purgés, gc --prune=now, puis contrôle : plus aucune occurrence dans aucun objet.

L'éditeur par type de fichier est écarté (2026-08-03)

Ajout du 2026-08-03, après vérification — le résidu annoncé plus bas n'existe pas

L'utilisateur ayant retiré le chiffrement de son config.yaml, il a pu être lu : aucune clé sidebarPlus.sftpEditors, et sftpEditorPath porte toujours son éditeur. La migration a tourné en mémoire pendant l'essai sans que rien n'atteigne le disque. Le paragraphe ci-dessous, écrit par précaution avant d'avoir pu vérifier, décrit donc un état qui n'a pas eu lieu — il est conservé tel quel, le journal étant en ajout seul, et c'est cette entrée qui fait foi.

À en retenir plus qu'un détail de nettoyage : une conséquence supposée d'un code retiré n'est pas un fait tant qu'elle n'a pas été observée, et une gouvernance qui l'écrit au futur crée une fausse piste pour la session suivante.

Chantier Éditeur par type de fichier, construit puis retiré le même jour. Décision de l'utilisateur à l'essai : « Ouvrir avec » du clic droit fait le travail plus directement, et la table extension → programme n'apportait rien qui vaille son coût. Branche non conservée, aucun code sur master — la fiche de roadmap porte la trace et le motif.

Ce que l'essai a établi, et qui manquait au cadrage : la règle retenue — un type inconnu demande plutôt que d'emprunter l'éditeur d'un autre type — pose une question à chaque nouvelle extension rencontrée, et son alternative (repli silencieux sur le défaut) revient au comportement d'avant. Aucune des deux positions ne justifiait la table.

Un reste à connaître : les configurations ayant servi au test portent une clé sidebarPlus.sftpEditors orpheline et un sftpEditorPath vidé par la migration. Le code revenu en arrière ignore la première ; l'éditeur se re-règle en un clic.

Latence et âge des sessions actives (2026-08-03)

Chantier Ping live, branche feat/ping-live, testé et validé par l'utilisateur le 2026-08-03.

La question à trancher n'était pas l'affichage mais la mesure : ni Tabby ni russh ne tiennent de temps d'aller-retour — vérifié sur le bundle installé, la seule mention de keepalive est l'intervalle passé à connect(), qui ne rend rien. Le chiffre est donc produit, et le moyen choisi est la définition de ce que la pastille signifie. Retenu après arbitrage de l'utilisateur : un stat() chronométré sur le canal SFTP déjà ouvert de la session. Il mesure l'aller-retour chiffré de cette session — le délai ressenti à la frappe — quand un ping ICMP mesure l'hôte et se fait couramment filtrer, et qu'une sonde TCP ouvrirait puis abandonnerait une connexion avant authentification à chaque tour, laissant une trace dans les logs du serveur et de quoi se faire bannir par son propre fail2ban.

Ce stat() ne contredit pas l'invariant du piège #50 : rien de la réponse n'est lu, seul le temps compte. La distinction est écrite dans le service pour qu'une relecture n'y voie pas une régression.

Trois retours de la passe de test ont refait la présentation. La pastille ajoutée au bout de la ligne était recouverte par le bouton SFTP au survol — et n'apportait rien, toutes les lignes de cette section étant vivantes par construction, donc la pastille de gauche était perpétuellement verte et muette. La latence a pris sa place. L'infobulle, d'abord posée sur la pastille, est passée sur la ligne entière : viser 6 px pour lire une valeur n'est pas un geste. Sa forme a été fixée à trois champs, app.exemple.fr | 13 ms | 1m 47s, ce qui a sorti le titre vivant de l'onglet — la ligne montre déjà le nom, et un titre qui suit le répertoire courant était la partie bavarde.

Un ajout non prévu, demandé en cours de passe : le temps écoulé depuis l'ouverture. Rien ne date une session dans Tabby, il est donc stampé à la première apparition dans la liste — avec les deux conséquences consignées dans la roadmap. Il ne dépend pas du réglage de latence.

Le sondage suit le passage de 2 s déjà en place plutôt que d'ajouter un timer, et ni la latence ni la durée ne sont portées par les lignes elles-mêmes : une valeur qui bouge ferait échouer sameSessions() et reconstruirait le DOM de chaque ligne deux fois par seconde, emportant le survol dont dépendent leurs boutons.

DateHashRésumé
2026-08-03a8f5c59 Infobulle figée sous le curseur. Réécrire un title détruit l'infobulle affichée : celle-ci clignotait à chaque passage de 2 s pendant qu'on la lisait (piège #69). Gelée tant que le pointeur reste sur la ligne.
2026-08-033c17f14 Infobulle concise à trois champs. Forme fixée par l'utilisateur ; le champ de latence disparaît faute de mesure plutôt que d'afficher une attente, et le service rend le nombre brut au lieu d'une phrase.
2026-08-0368e81c7 Infobulle sur la ligne entière, plus sur la pastille — l'agrandissement de sa zone de survol disparaît avec la raison qui l'avait fait ajouter.
2026-08-03a9c56aa Latence portée par la pastille existante, et âge de la session. La seconde pastille est retirée ; le temps écoulé s'affiche sur la ligne et dans l'infobulle.
2026-08-03161074c Sonde de latence (src/ping.service.ts) : stat() chronométré sur le canal SFTP de la session, réglage pingIntervalSeconds à 0 par défaut, serveur sans canal SFTP marqué non mesurable une fois pour toutes.

Barre de filtrage : raccourci, portée, et un ordre qu'on ne persiste plus (2026-08-03)

Chantier Barre de filtrage rapide, branche feat/filtre-rapide, testé et validé par l'utilisateur le 2026-08-03.

Le champ existait déjà — la fiche de roadmap décrivait un chantier neuf alors que l'essentiel était en place depuis le socle. Ce qui manquait vraiment : le raccourci, l'échappement, et une portée de recherche qui s'arrêtait au nom et à la description des profils. Constat fait en lisant le code plutôt qu'en croyant la fiche, et arbitré avec l'utilisateur avant d'écrire.

Ctrl+F est libre : Tabby met la recherche du terminal sur Ctrl+Shift+F hors macOS. Le raccourci passe par hotkey$ et non unfilteredHotkey$, donc il reste muet pendant qu'on tape dans un champ — y compris celui qu'il sert à atteindre. Depuis la vue SFTP il bascule d'abord sur Profils : le champ n'y existe pas, et ne rien faire ferait passer le raccourci pour cassé.

Un bug préexistant est tombé avec ce chantier, et c'est le point qui compte : glisser un profil pendant qu'un filtre est actif persistait l'ordre des résultats comme ordre réel — sur « Tous », directement dans les weight natifs des profils, dans config.yaml, sans un mot. Les résultats ne sont pas l'arbre, mais les directives CDK de la template n'en savent rien. Le défaut existait avant, et l'ajout des dossiers dans les résultats rendait le geste bien plus facile à déclencher : le glisser est donc coupé tant qu'un filtre est actif (piège #68).

DateHashRésumé
2026-08-036de353c Raccourci, échappement et portée de la barre de filtre. Ctrl+F place le curseur et sélectionne le contenu, Échap vide et rend le focus ; la recherche couvre options.host/options.user et rend les dossiers correspondants au-dessus des profils, avec leur contenu. Glisser-déposer désactivé pendant qu'un filtre est actif (piège #68).

Auto-Enter sort du périmètre (2026-08-03)

Décision de l'utilisateur, sans code. Le chantier Auto-Enter rejoint la section hors périmètre, son design conservé tel quel.

Deux motifs, à ne pas confondre. L'utilité n'est pas établie : le design valide une invite dont le mot de passe est pré-rempli, or Tabby envoie déjà ce mot de passe quand l'authentification enregistrée fonctionne — ne resteraient que les invites qu'il ne traite pas nativement, qui sont précisément celles où un Entrée parasite atteindrait un shell de production. Sans invite précise à nommer, le chantier n'a pas de cible. Le rattachement est discuté : l'utilisateur le verrait du côté de tabby-better-vault ; la roadmap note que le geste décrit est d'écrire dans un pty distant et non de lire un secret, donc que le rapprochement mérite examen plutôt que d'être supposé — et que cet examen se fait dans une session consacrée à l'autre projet.

Écart avec le précédent du Vault Auto-Unlock, qui est ce qui a justifié de garder le design ici : celui-là sortait vers un périmètre d'accueil déjà décidé, et son design a été transporté. Celui-ci sort sans destination acquise.

DateHashRésumé
2026-08-03ceef2d9 Auto-Enter passe hors périmètre. Section déplacée avec son design, statut du tableau de bord mis à jour, et les trois renvois qui le traitaient comme un chantier à venir corrigés — dont les logs de son moteur, attendus par le futur onglet de réglages.

Plus de « terminé » avant que le système ait fini (2026-08-03)

Point 4 du chantier Panneau SFTP : suivre l'état réel de la session, sur la branche fix/etat-transitoire — dernier point ouvert, le chantier est clos avec lui.

L'arbitrage d'abord : le marqueur venait de rendre l'unification possible — passer aussi les fichiers par lui aurait donné une fin réelle et une destination connue. Écarté, et pour une raison de fond plutôt que par prudence : le marqueur suppose que la destination est un dossier du système de fichiers. Un dossier glissé n'a pas d'autre cible plausible ; un fichier, si — une zone d'envoi de navigateur, un client de messagerie — et celles-là auraient reçu le marqueur vide pendant que le fichier serait parti dans le dossier de repli. Le cas fichier marchait, il n'y avait pas lieu de le faire dépendre d'une heuristique.

Retenu : un état entre la fin de notre flux et la remise réelle, levé sur une durée estimée d'après la taille. Le sens de l'erreur a été choisi — 200 Mo/s supposés là où 250 avaient été mesurés (#58), donc la ligne finit tard plutôt que tôt, l'objet même de la manœuvre étant de ne plus annoncer en place un fichier qui ne peut pas l'être.

Un état terminal peut encore avoir besoin du minuteur, et c'est le seul vrai piège de ce chantier. « Ce qui peut être annulé » et « ce qui a besoin du tick » ne sont pas la même population : une ligne en remise n'a plus rien d'annulable de notre côté, mais son échéance n'est surveillée par rien d'autre, si bien qu'arrêter le minuteur au dernier transfert actif la laissait figée pour le reste de la session. Vérifié au test, avec la conséquence visible attendue : vider la liste pendant une remise ne demande pas confirmation et ne touche pas au fichier déposé.

DateHashRésumé
2026-08-031ded769 Une ligne n'annonce plus « terminé » tant que le système écrit encore. État handover sur la seule voie qui délègue l'écriture au shell — la voie marqueur écrit elle-même, à un chemin choisi, et n'est pas concernée. Durée estimée d'après la taille, plancher 400 ms, plafond 2 min, infobulle qui dit l'approximation. runningCount sépare ce qui a besoin du tick de ce qui peut encore être annulé.

Marqueur de dépôt : un dossier sort en un seul geste (2026-08-03)

Chantier Marqueur de dépôt, sur la branche feat/marqueur-depot, ouvert par l'étude du même jour. Le design est dans la roadmap ; ce qui suit est ce que la construction et le test ont appris.

L'inversion tient en une phrase : ne plus annoncer la charge utile, mais un fichier vide au nom unique. Le shell l'écrit à l'endroit du dépôt, et le retrouver donne la destination — que Windows ne communique jamais à l'application source (#58). Ce qui n'était pas prévu et qui compte autant : le geste redevient un glisser HTML5 ordinaire, donc le déplacement interne et la sortie vers l'OS cessent de se disputer le même geste. Tout l'échafaudage du glisser en deux temps — intention retenue d'un geste sur l'autre, surveillance de la sortie de fenêtre, notice « glissez-le à nouveau » — devient un repli, plus un chemin ordinaire.

Le socle a été mesuré avant d'être écrit : énumérer les fenêtres de l'Explorateur par Shell.Application en COM répond en ~50 ms et voit bien une fenêtre ouverte. C'est le point unique de rupture du mécanisme, et il ne valait pas d'écrire trois cents lignes avant de le savoir.

Ce que le test a établi (cinq cas au geste réel, tous validés) : dépôt dans une fenêtre, sur le Bureau, sur l'icône d'un dossier, déplacement interne inchangé, glisser d'un fichier inchangé. Et un cas que le design n'avait pas prévu — déposer sur le Terminal Windows ou MobaXterm ne produit rien, ces cibles ne réclamant jamais l'URL. Le repli n'a donc pas à se déclencher, et c'est bien ce qu'on veut (#67). Le repli lui-même reste non exercé.

La traversée d'arborescence a été sortie de sftpDragOut vers remoteTree.ts pour être partagée avec la livraison du marqueur, symlinks compris — deux copies auraient été deux occasions de n'en corriger qu'une.

DateHashRésumé
2026-08-0303cab1a Un dossier sort en un seul geste, et on sait où il est allé. Offre DownloadURL servant un marqueur vide au nom unique ; sa requête HTTP est le signal du dépôt ; le marqueur est cherché dans le Bureau, les Téléchargements, les fenêtres de l'Explorateur ouvertes et un niveau de sous-dossiers, puis effacé, et l'entrée livrée là par SFTP. Collision renommée à la façon de l'Explorateur. Repli sur ~/Downloads si la destination est introuvable, et sur l'ancien startDrag() si le serveur local ne démarre pas.

Étude des plugins SFTP existants (2026-08-03)

Préalable posé par l'utilisateur au moment de valider le volet 3 : savoir ce qui est déjà résolu ailleurs avant d'écrire davantage de mécanique de glisser. Aucun code produit — le résultat est consigné dans la roadmap et a rouvert deux points qui y étaient donnés pour clos.

Trois plugins lus dans leurs sources d'origine : tabby-sftp-plus 2.0.0 (installé sur le poste), tabby-sftp-panel 0.3.0 et tabby-sftp-ui 0.2.7, récupérés depuis npm hors du dépôt. Tous trois publient leur index.js.map avec sourcesContent : les sources TypeScript s'en extraient en entier, commentaires d'implémentation compris. C'est bien plus sûr que la lecture de bundle compilé pratiquée jusqu'ici, et c'est ce qui a permis de lire les raisons de leurs choix, pas seulement leurs effets.

Ce que l'étude a confirmé : notre glisser sortant par DownloadURL + serveur HTTP local est exactement celui de tabby-sftp-panel, jusqu'à l'impératif de Content-Length exact — convergence indépendante, choix à ne pas rejuger. Et le geste en deux temps de tabby-sftp-plus, qui pré-copie avant de pouvoir annoncer quoi que ce soit, montre que notre « glissez-le à nouveau » décrivait une propriété du mécanisme.

Ce qu'elle a invalidé : le piège #58 concluait qu'un contrôle « le fichier est-il bien arrivé ? » était impossible pour un glisser sortant. Vrai de l'API, faux en pratique — tabby-sftp-panel fait écrire par le shell un marqueur de 0 octet au nom unique, puis le retrouve pour apprendre la destination. Le piège est corrigé et le chantier correspondant ouvert ; il rend sans objet les deux retours de test du volet 3 et fournit au point 4 du panneau d'état le signal de fin qui lui manquait. Relevé au passage : tabby-sftp-ui déplace lui aussi une entrée par dépôt sur une ligne de dossier, avec nos deux garde-fous mais sans regarder la collision, et il porte la troncature du piège #64 que notre traversée évite déjà.

DateHashRésumé
2026-08-034e25209 Étude consignée : bilan des trois plugins en tête du chantier Transferts, chantier Marqueur de dépôt ouvert en proposition, point 4 du panneau d'état rouvert et suspendu à cet arbitrage, piège #58 corrigé sur son point le plus large.

Déplacer une entrée distante au glisser-déposer (2026-08-02)

Volet 3 du chantier Transferts SFTP, sur la branche feat/deplacement-interne-sftp. Le design est dans la roadmap ; ce qui suit est ce que la construction a appris.

Le geste de départ était déjà occupé par le glisser sortant, et les deux natures d'entrée ne se partagent pas pareil : un fichier annonce sa copie par un DownloadURL, qui voyage dans le glisser HTML5 — le type maison s'ajoute à côté et rien n'a à être décidé au départ. Un dossier, lui, réclame preventDefault() puis webContents.startDrag(), donc le geste entier. L'utilisateur ayant sftpDragOutFolders activé, le laisser prioritaire aurait supprimé en silence ce que ce volet vient ajouter. Arbitrage rendu par l'utilisateur : les deux, en deux temps — glisser un dossier le déplace ; le sortir de la fenêtre et le lâcher là démarre sa copie, et le geste suivant l'emporte. La sortie de fenêtre se lit à l'âge du dernier dragover reçu par le document.

Passe de test sur un serveur réel (machine VPN, autonomie accordée par l'utilisateur), dans /tmp/zzz-test-move/ créé pour l'occasion, rien touché en dehors. Neuf vérifications, dont les trois refus au survol — dossier visé sur lui-même, entrée lâchée dans le dossier où elle est déjà — sans le moindre écrit sur le serveur, le refus d'une collision, le déplacement à travers un lien symbolique vers un dossier, et un lien cassé qui ne déplace rien et le dit. Le curseur, lui, ne s'y vérifie pas (#66) : c'est la passe manuelle de l'utilisateur qui a validé l'usage, et qui a rapporté les deux réserves consignées en roadmap.

Trois gels du renderer avant d'y arriver, tous dus au harnais et non au plugin : piloter un glisser sur une ligne finit par appeler startDrag(), dont la boucle OLE attend une souris qui n'existe pas (#65). C'est ce qui a révélé au passage que le réglage était activé côté utilisateur, donc le conflit ci-dessus.

DateHashRésumé
2026-08-020114245 Une entrée distante se déplace en la glissant sur un dossier. Le glisser annonce application/x-tabby-sftp-path ; survol et dépôt distinguent l'interne de l'entrant sur les types, jamais sur la position. rename() côté serveur : rien ne transite. Garde-fous — dossier dans lui-même ou dans un de ses descendants, entrée déjà dans la cible, destination occupée — refusés en amont, sans preventDefault(), donc le dépôt ne peut pas avoir lieu. Un dossier garde son glisser HTML5 et ne part vers l'OS qu'au geste suivant une sortie de fenêtre.

Dépôt depuis l'OS sur un dossier distant visé (2026-08-02)

Volet 2 du chantier Transferts SFTP, sur la branche feat/depot-sur-dossier-distant. Testé de bout en bout sur un serveur réel — pilotage par l'injecteur Angular depuis le CDP (#8), dans un dossier jetable supprimé après coup — puis validé par l'utilisateur au vrai geste, glisser depuis l'Explorateur.

DateHashRésumé
2026-08-0245a8ecf Le survol et la sélection étaient invisibles une ligne sur deux. Défaut trouvé en écrivant la règle de la ligne ciblée, qui l'aurait hérité. La règle des zébrures s'écrit .sftp-grid.with-zebra .sftp-zebra, soit deux classes plus la porte — (0,3,1) — quand le survol et la sélection sont à (0,2,1). Sur une ligne impaire, et les zébrures sont actives par défaut, la couleur d'accent était donc calculée puis perdue dans la cascade. Ce qui ne se lit pas comme un défaut mais comme une liste qui ne s'allume qu'une ligne sur deux, d'où des mois sans qu'il soit vu. Vérifié sur les sélecteurs réellement émis dans le bundle, pas sur le SCSS source, et mesuré au getComputedStyle : en réinjectant l'ancien sélecteur avec une couleur témoin, c'est bien lui qui gagne. La porte passe en :where(), qui n'apporte aucune spécificité — la zébrure tombe à (0,1,1) et tout état peint par-dessus.
2026-08-0226c152c Un dépôt atterrit désormais où il est lâché, et n'écrase plus rien en silence. La dropZone de tabby-core n'émet que le DirectoryUpload, jamais le DragEvent : la position du curseur — donc la ligne visée — lui échappe par construction. Le panneau gère dragover/drop lui-même, en un seul point du corps, ce qui donne aussi la main sur le retour visuel. Ligne de dossier → ce dossier, ligne .. → le parent, tout le reste → le dossier affiché. Un lien symbolique est suivi au dépôt et non au survol, et quand il ne mène pas à un dossier le dépôt retombe sur le dossier affiché en le disant. SFTPSession.upload() renommant par-dessus sa cible, chaque dossier de destination est listé une fois (readdir, jamais stat(), #50) et les collisions sont confirmées en une seule question : rien n'est écrit avant la réponse, pas même un mkdir.
2026-08-0226c152c Et la traversée de l'arborescence déposée devient la nôtre. startUploadFromDragEvent(), que la roadmap recommandait de réutiliser, appelle readEntries() une seule fois par dossier — or l'API ne rend qu'une tranche, 100 entrées sous Chromium, et ne signale la fin que par une réponse vide. Au-delà, le reste partait à la poubelle sans erreur, et le récapitulatif comptait ce qu'il avait vu. Mesuré sur la même arborescence de 252 fichiers : 101 contre 252 (#64). Deux effets de bord bienvenus : cette méthode publiait chaque fichier sur fileTransferStarted$ pendant le parcours, donc avant toute confirmation — le test l'a montré en laissant 101 lignes « en cours » que rien ne terminait — et appRoot y étant abonné, chaque dépôt ouvrait le menu de transferts natif. Nos HTMLFileUpload sont désormais construits et inscrits au registre au moment où ils démarrent, un à la fois.

Revue générale, remise en ordre des documents, et intégrité des données (2026-08-02)

Revue complète demandée sur le code, la gouvernance et les documents de production. Deux branches : chore/mise-en-ordre-doc-et-release pour ce qui était de la mise en ordre, puis fix/integrite-donnees pour le premier des trois chantiers que la revue a ouverts (#integrite-donnees).

DateHashRésumé
2026-08-02cfe6010 Sept défauts de cycle de vie, dont deux visibles. SidebarPlusMountService.unmount() détachait la vue et détruisait le composant sans retirer le nœud : le renderer DOM d'Angular laisse destroyNode nul, donc éteindre le plugin laissait une sidebar figée à l'écran, et le rallumer en insérait une seconde à côté du cadavre. Et la purge des copies temporaires prenait pour repère process.uptime() — l'âge du renderer courant, pas de l'application : une fenêtre ouverte à 10h30 jugeait « antérieur au démarrage » le dossier de la fenêtre de 10h00 et l'effaçait, fichier en cours d'édition compris, après quoi l'enregistrement suivant partait silencieusement nulle part. Elle demande désormais si le processus nommé dans le dossier vit encore.
2026-08-02cfe6010 Cinq défauts plus discrets du même voisinage. Le Content-Length du glisser sortant venait de la ligne affichée et n'était jamais revérifié au dépôt, si bien qu'un fichier ayant changé de taille depuis le dernier readdir était servi avec un en-tête qui contredisait son corps — ce qui, pour le receveur, est la définition d'un téléchargement tronqué. Les offres périmées n'étaient purgées que par un glisser suivant, chacune retenant sa session SFTP jusqu'à dix minutes ; chaque requête les balaie maintenant. Chaque préparation de glisser laissait son dossier temporaire quand une copie périmée était remplacée. La promesse de démarrage du serveur ne se résolvait jamais si la socket refusait de se lier. Et SftpRemoteEditor était le seul collaborateur à ne pas recevoir NgZone, alors que toute sa chaîne part d'un callback fs.watch que zone.js ne patche pas : sa modale de conflit et ses notifications n'étaient peintes qu'au prochain cycle déclenché par autre chose (#41).
2026-08-02b4159f1 Éditer un lien symbolique éditait le lien, avec quatre conséquences. openEntry() résolvait le lien par sftp.stat(), dont le mode revient à 0 (#50) : ce 0 partait dans platform.startDownload(name, mode, …), qui fait fs.open(path, 'w', mode), donc la copie locale atterrissait en 444 et l'éditeur refusait d'enregistrer. Si l'éditeur forçait l'écriture, le mode restauré après envoi était celui du lien, 0o120777 — dont & 0o7777 donne 0o777 : le fichier distant finissait accessible en écriture à tout le monde, le mécanisme du #53 se retournant sur ce cas. La détection de conflit livrée la veille ne pouvait pas fonctionner, comparant taille et date du lien, qui ne bougent jamais quand la cible change. Et SFTPSession.upload() faisant unlink avant rename, chaque enregistrement détruisait le lien. Le lien est désormais résolu une fois par readlink() + readRemoteEntry(), et c'est la cible qui est éditée, chemin compris — les quatre tombent ensemble. Au passage, le dernier stat() d'observation de src/ disparaît : createFileFromMenu() testait un nom libre par stat(), qui suit les liens, si bien qu'un lien cassé du même nom faisait croire le nom disponible.
2026-08-023128e55 Trois pertes silencieuses dans les gestes qui réécrivent config.store. reparentGroup() recréait le dossier déplacé en recopiant quatre champs choisis à la main ; ProfileGroup en porte un cinquième, defaults, que Tabby fusionne dans le ConfigProxy de chaque profil du dossier — déplacer un dossier dépouillait donc ses profils de tout ce qu'ils héritaient de lui, et récursivement. Le groupe est désormais recopié en entier moins id, profiles et children. migrateWorkspaceGroupId() ne migrait ni favoriteGroups ni l'état replié du localStorage : un dossier favori perdait son étoile et revenait déplié. onProfileDrop() persistait l'ordre du conteneur source même quand celui-ci est une vue — CDK résolvant la cible parmi les listes connectées à la source, on peut sortir un profil des « Épinglés » sans pouvoir rien y déposer, et sur « Tous » cela réécrivait le weight de tous les profils encore épinglés. Enfin deleteGroup() comptait le contenu du nœud affiché, déjà filtré par le workspace actif : un dossier dont tout le contenu y est masqué passait le garde-fou, et deleteProfileGroup() renvoyait ses profils dans « Ungrouped ». Le comptage passe par rawGroupsSnapshot, non filtré, et le refus dit explicitement quand le contenu est masqué. L'abonnement à config.changed$ est aussi résilié à la destruction du composant.
2026-08-02d9d291b Préparation de la 0.3.0. Le README.md public et la description npm décrivaient le plugin d'avant le SFTP — workspaces et SFTP annoncés comme non faits alors qu'ils sont livrés depuis le 2026-07-29, rien des 94 commits suivants, et deux renvois vers tabby_sidebar_roadmap.md, découpé en .AIRules/ depuis. prepublishOnly passe par un build:prod ; filesize rejoint les devDependencies, webpack le bundlant déjà. Rien n'a été publié.
2026-08-0203ed7fd Un octet NUL rendait sftpBrowser.component.ts invisible aux recherches. Le séparateur de la clé de cache des colonnes était écrit brut dans le source ; rg et grep classent alors le fichier entier comme binaire et le sautent en silence — 1136 lignes hors de portée de toute recherche de code, ce qui s'est produit pendant cette revue même. Écrit '\0'. Sept commentaires qui décrivaient autre chose que le code ont été repris dans le même geste, dont celui de tempFiles qui affirmait l'inverse du comportement réel, trois mentions de stat() là où tout passe par readRemoteEntry(), et la justification de onze getters par un échappement pug qui n'existe pas — vérifié en compilant le template, seize bindings du même fichier utilisent des littéraux entre guillemets et fonctionnent.
2026-08-0225dbd4c Dérives de gouvernance corrigées, et ce que la revue a trouvé consigné. CLAUDE.md décrivait le correctif min-height: 8px inconditionnel du #19, remplacé depuis par celui du #26AI-CONTEXT avait été corrigé le 2026-07-31, pas le fichier d'instructions, ce qu'A-11 vise précisément. Il annonçait aussi une remise à niveau inachevée alors que ses cinq items étaient faits. Le sommaire d'AI-CONTEXT ignorait les pièges #59, #60 et #61. Côté roadmap, six contradictions internes levées, dont un chapô disant « Rien n'est implémenté » quatre lignes au-dessus d'un volet marqué Fait. Trois chantiers ouverts pour arbitrage : #integrite-donnees, #liens-symboliques-edition et #cycle-de-vie.
2026-08-02n/a (opération Git) Restes de la purge d'attribution du 2026-08-01 nettoyés. La branche backup-avant-purge-attribution était la « fausse sauvegarde » que décrit #11 : filter-branch l'avait réécrite elle aussi, elle était ancêtre de master et ne contenait rien d'unique. Les refs/original/, seul vrai filet, portaient encore 63 mentions d'attribution. Avant de les supprimer, le contrôle du #11 a été refait sous une autre forme — master ayant avancé de 32 commits depuis, un diff direct ne prouvait plus rien : l'arbre du sommet d'avant purge a été retrouvé à l'identique dans master (dd51fe0), ce qui établit que la réécriture n'avait touché que les messages. Le dépôt ne porte plus aucune métadonnée d'attribution.

Passe de test du 2026-08-02 — pilotée par l'assistant, à la demande de l'utilisateur

Menée dans Tabby lancé en --debug --remote-debugging-port=9333, sur des entrées grp-zzz-test-* exclusivement, avec un instantané complet de groups, profiles et sidebarPlus pris avant tout geste. L'instance vivante du composant s'atteint par getPlatform()._modules[0]._r3Injector puis les vues attachées à l'ApplicationRefwindow.ng ne sert à rien, le build étant en production et ngDevMode à false. État final comparé champ à champ à l'instantané : identique.

Passe de test du 2026-08-02 sur les liens symboliques — serveur réel

Menée sur un serveur de l'utilisateur, avec son autorisation explicite, dans /tmp/zzz-test-symlink/ — répertoire créé pour l'occasion et supprimé après coup, rien touché en dehors. Une cible en 0o640, un lien vers elle, et un lien cassé.

Sept vérifications sur sept, round-trip complet inclus : c'est la cible qui part à l'éditeur, avec son mode réel 0o640 ; la copie locale est inscriptible ; le lien cassé est annoncé au lieu d'être subi ; et après téléchargement, modification et renvoi, le lien existe toujours et la cible a gardé 0o640.

La contre-preuve a été mesurée sur ce même serveur plutôt que supposée : stat() du lien rend un mode 0o0 et une date au 1970-01-01T00:00:00Z, et le chmod qu'appliquait l'ancien chemin aurait bien été 0o777.

Une décision prise en cours de passe

La connexion a demandé une vérification de clé d'hôte. « Accepter uniquement cette fois » a été choisi plutôt que « Accepter et mémoriser » : graver une décision de confiance dans la configuration de l'utilisateur n'était pas nécessaire au test. Le mot de passe, lui, a été saisi par l'utilisateur — aucun secret n'a transité par l'assistant.

Un faux négatif, dû au harnais

La première passe concluait que les profils ne suivaient pas leur dossier. C'était le harnais : il appelait getProfileGroups() sans options, ce qui ne remplit pas profiles, là où le composant passe { includeNonUserGroup: true, includeProfiles: true }. À retenir pour toute passe future qui reconstitue un appel du composant : reprendre ses options, pas seulement son nom de méthode.

Passe de test du 2026-08-02 sur le cycle de vie

Deux des sept points ont été exercés pour de bon, les deux visibles.

Démontage : éteindre puis rallumer le plugin, deux cycles de suite, laisse exactement une sidebar dans le DOM et une vue attachée à l'ApplicationRef. Avant le correctif, l'extinction en laissait une figée à l'écran et le rallumage en ajoutait une seconde.

Purge des temporaires : deux dossiers témoins posés à la main, l'un nommé d'après un pid mort, l'autre d'après un processus vivant, puis Tabby relancé. Le premier est purgé, le second conservé — c'est ce second cas que l'ancien repère temporel manquait, puisqu'il jugeait « antérieur au démarrage » tout ce qui précédait le renderer courant.

Ce qui n'a pas été exercé

La revalidation du Content-Length au dépôt et la promesse de démarrage du serveur quand la socket refuse de se lier : deux chemins d'erreur ou de course, sans scénario reproductible à peu de frais. Le code est en place, aucune passe ne l'a parcouru — à ne pas présenter comme validé.

Un placeholder résolu dans une entrée ancienne

La cellule Hash du chantier Tunnels SSH (2026-07-29) portait (ce commit), jamais remplacé — c'est ce que l'audit du 2026-07-31 avait manqué en cherchant la formule « commit en cours ». Le commit visé est c4422f6, qui a livré les tunnels et écrit cette entrée. Il a été inscrit : le placeholder était fait pour l'être, donc le résoudre achève l'entrée plutôt que de la réécrire — A-4 reste tenu.

Transferts : montrer qu'un transfert est mort, et le servir nous-mêmes (2026-08-02)

Branche fix/transferts-interrompus. Ouverte sur le point 3 de #panneau-etat-session — « demander confirmation si un transfert est en cours au moment de la bascule » — dont la vérification a montré que la prémisse était fausse. Ce qui a été livré à la place répond au besoin réel : savoir qu'un transfert est mort.

DateHashRésumé
2026-08-02a366696 Un client qui s'en va arrête vraiment le transfert, et la ligne le dit. Signalé en test : annuler la boîte « Enregistrer sous » laissait une ligne « en cours » pour un téléchargement jamais lancé du point de vue de l'utilisateur. Chromium réclame les octets avant d'afficher sa boîte, donc la réponse est déjà servie quand il renonce — et deux défauts se cumulaient. res.write() sur une réponse détruite rend false, et l'attente de drain qui suit ne se résolvait jamais : boucle suspendue à vie, ligne figée, canal SFTP tenu ouvert dessus. Et rien ne l'aurait arrêtée de toute façon — SFTPSession.download() ne consulte jamais isCancelled(), donc annuler un FileTransfer ne l'interrompt pas ; seule une exception levée depuis notre write() le fait (#61). La ligne distingue désormais les deux causes : client parti = annulé (writableFinished faux), transport mort = interrompu.
2026-08-02e73e578 Le téléchargement du menu contextuel passe par notre offre HTTP. Un transfert coupé s'affichait « annulé » par ce chemin et « interrompu » par le glisser sortant — même cause, deux libellés, et le plus courant des deux mentait. La voie héritée appelle this.sftp.download(...) sans l'attendre (sftpPanel.component.ts:252 de l'app installée), donc son rejet n'est tenu par personne : la seule trace qui nous parvienne est le transfer.cancel() que tabby-ssh fait au passage. Servir les octets nous-mêmes remet le rejet dans un catch qui est le nôtre. Chromium écrit le fichier, Tabby n'installant aucun gestionnaire will-download — c'est donc la boîte « Enregistrer sous » d'Electron qui s'ouvre, comme avant. Arbitrage tranché par l'utilisateur : on renonce à la progression du menu de transferts natif — masqué par défaut — pour gagner un échec visible. Point de veille fragile-8 ajouté à la checklist d'après-update.
2026-08-028cff07d Les cases à cocher des réglages deviennent visibles. Signalé en test : « Masquer le menu des transferts de Tabby » n'affichait aucune case. Ce n'était pas une régression — rien n'avait bougé dans ce bloc depuis son arrivée, et le template compilé contenait bien l'input. Bootstrap ne dessine son interrupteur que dans un conteneur .form-check.form-switch : un input.form-check-input nu rend un carré transparent, invisible une fois décoché sur le thème sombre, visible une fois coché. Le réglage ayant été décoché la veille pour vérifier le retour du menu natif, il avait disparu de l'écran — ce qui se lit comme un contrôle manquant, pas comme un contrôle non coché. Les deux cases de la page passent par toggle, le composant de tabby-core, vérifié dans les exports du bundle installé avant usage (#13). #60.
2026-08-02ca6dde1 Un transfert tué ne reste plus « en cours » à vie, et dispose() n'efface plus sous les pieds d'un transfert. Le registre ne change d'état que sur isCancelled() ou isComplete(), auxquels un transfert dont le transport a disparu répond non deux fois : sa ligne restait active pour toujours, figée à son pourcentage. Seul l'appelant qui attend la promesse voit le rejet, donc c'est lui qui le signale — nouvel état failed, ligne « interrompu à N % » avec la raison en infobulle. Second défaut trouvé au même endroit : dispose() supprimait les dossiers temporaires sans regarder si une copie de glisser ou un renvoi d'édition y écrivait encore ; Windows refuse d'effacer un fichier ouvert et l'échec était avalé, donc le symptôme était un temporaire abandonné, silencieusement, et précisément quand une coupure détruit le panneau en plein enregistrement. Les deux dispose() attendent maintenant ce qui est en vol.
Trois faits à ne pas redécouvrir

La voie Chromium transfère pendant que la boîte « Enregistrer sous » est ouverte. Le pourcentage monte avant toute validation, et les octets d'une annulation sont perdus — rien ne s'écrit chez l'utilisateur, Chromium jetant son tampon. Caractéristique assumée, constatée en test et acceptée : l'alternative (platform.startDownload() attendu, qui n'ouvre le flux qu'après le choix du dossier) a été proposée puis écartée, l'annulation coupant réellement le transfert.

downloadFolder reste sur la voie héritée : une offre DownloadURL sert exactement un fichier. « Download directory » affichera donc encore « annulé » sur une coupure — écart connu, pas un oubli.

Le libellé « annulé » n'était pas un défaut d'affichage mais l'absence d'un await côté natif. Chercher la cause dans le registre aurait été une impasse.

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

Branche fix/sftp-session-perdue. Porte les points 1 et 2 du chantier #panneau-etat-session : le panneau qui restait mort après une perte de connexion, et la sidebar qui rend la main aux Profils quand la session liée n'est plus exploitable. Les points 3 et 4 restent ouverts.

DateHashRésumé
2026-08-02697a624 Trois secondes de grâce avant de quitter la vue, et une notice quand elle est quittée. Sans délai, l'issue dépendait de l'instant du tick : le bouton Reconnect de l'onglet remonte une session en moins d'une seconde, donc tantôt le panneau se reconstruisait sur place avec son dossier, tantôt la sidebar basculait sur les Profils — deux comportements pour le même geste, au hasard de la course. Une perte n'agit donc plus qu'au bout de SESSION_LOSS_GRACE_MS ; une reconnexion qui rentre dans cette fenêtre est traitée pour ce qu'elle est, une reconstruction, et la vue ne bouge pas. Contrepartie assumée : pendant ces trois secondes le panneau reste affiché mais inerte — un geste y répond « Session closed ». La notice, demandée par l'utilisateur, passe par le service maison, seul canal dont la durée est réglable (#51). Délai retenu avec lui après mesure du comportement non déterministe.
2026-08-0211d3d54 Le panneau reconstruit retrouve son dossier. Régression du commit précédent, relevée en test : détruire le panneau jette son path, et le cache donnait jusque-là cette mémoire de navigation gratuitement — on repartait donc de la racine après chaque coupure. Le dernier dossier est retenu par onglet avant destruction et réinjecté avant l'attache, dans la même fenêtre que session : c'est ngOnInit qui lit les deux. Si le dossier a disparu entre-temps, le ngOnInit hérité notifie et retombe sur / tout seul — rien à ajouter de notre côté. La table est purgée avec les onglets fermés.
2026-08-022aa610b La vraie cause : on comparait des onglets, pas des sessions. Le correctif de 477b7e5 était posé sur un chemin de code jamais emprunté — le panneau restait mort exactement comme avant. sync() sortait sur tab === this.boundTab, or un onglet qui perd sa connexion et se reconnecte est le même objet SSHTabComponent : seul tab.sshSession est remplacé, donc attachPanel() n'était jamais rappelé et la comparaison d'identité qu'il porte jamais évaluée. Le garde-fou qui aurait dû forcer un passage par le placeholder ne se déclenchait pas davantage : resolveFocusedSSHTab() testait sshSession?.open, et ce drapeau ment — jamais remis à false (#59). Le test honnête existait déjà à côté, pour « Sessions actives » (#37) : il est désormais porté par isLiveSSHTab() dans src/tabs.ts et appelé par les deux blocs, qui avaient divergé — la liste retirait une session que le panneau continuait de servir, l'incohérence relevée au point 2 du chantier. Livré avec, le point 2 lui-même : la sidebar rend la main aux Profils quand la session liée est perdue ou quand le canal n'a jamais pu s'ouvrir (#57), sans retour automatique. Le panneau mort est détruit et non mis en cache — il tient un canal SFTP sur un transport disparu.
2026-08-02477b7e5 Première tentative : reconstruire le panneau quand la session a changé. attachPanel() compare l'identité de la session et détruit le panneau en cache quand elle diffère ; super.ngOnInit() est entouré d'un try/catch, l'échec d'ouverture du canal devenant l'état sftpUnavailable lisible par le panneau hôte (#57). Les deux morceaux sont bons et ont été conservés, mais aucun n'était atteint : voir 2aa610b. Consigne au passage le piège #58.
Ce que la passe de test a établi, et ce qu'elle n'a pas pu établir

La reproduction écrite au design était fausse : 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 sont le bouton Reconnect de la barre d'outils de l'onglet (reconnexion rapide, sous la grâce) et sudo ss -K state established '( sport = :22 )' (perte durable, jusqu'à la touche de reconnexion).

La bascule sur exit n'a pas été observée, faute de pouvoir l'être : le behaviorOnSessionEnd de l'utilisateur ferme l'onglet, qui part donc avec son panneau et est nettoyé par pruneClosedTabs(). Le code la couvre — isLiveSSHTab() lit le shell, pas le transport — mais elle reste non exercée tant qu'un profil n'est pas réglé sur « garder l'onglet ouvert ». L'incohérence avec « Sessions actives » que décrivait le design n'est pas davantage observable dans cette configuration.

Validé en test manuel le 2026-08-02 : reconnexion rapide sans bascule ni perte de dossier, perte durable suivie de la bascule et de sa notice, panneau de nouveau opérationnel au retour, et non-régression du cache par onglet.

Glisser-déposer sortant réel, gestionnaire de transferts et réglages (2026-08-01 → 2026-08-02)

Branche feat/transferts-ui, ouverte le 2026-08-01 et mise en pause en cours de passe de test, reprise et terminée le 2026-08-02. Elle porte le volet 1 du chantier #transferts et l'aboutissement de #drag-out : le téléchargement démarre enfin au dépôt, ce que startDrag() ne permettait pas.

DateHashRésumé
2026-08-026d0bad2 Le transfert se voit au dépôt, l'ETA remplace le silence, et le menu natif s'efface. Le prototype DownloadURL marchait mais ne disait rien : quelques ko passent avant qu'on s'en aperçoive, quelques centaines de Mo font une minute de silence complet. HttpFileDownload étant construit à la main, il ne passait par personne — ni la sidebar ni la tab-bar, toutes deux abonnées à fileTransferStarted$. Il est désormais inscrit au registre quand la requête HTTP arrive, donc au dépôt, jamais pour un geste abandonné. Inscrit, et non publié sur le flux natif : appRoot ouvre son dropdown à chaque émission, ce qui aurait posé le menu de Tabby par-dessus les onglets à chaque glisser, et ses lignes ne s'effacent qu'une par une — masqué, il aurait grossi sans recours. D'où le réglage qui le masque, coché par défaut, visant l'hôte du dropdown et transfers-menu, que container='body' téléporte hors du bouton. Mesure contre commentaire : le Content-Length ne donne aucune barre de progression sous Windows — les octets sont écrits par Chromium comme un téléchargement, et Electron n'a pas d'indicateur ; le commentaire qui prétendait l'inverse est corrigé. Côté ligne : temps écoulé au lieu de l'heure de début, colonne ETA tant qu'elle est honnête, et vitesse recalculée ici, getSpeed() ne mesurant que le dernier bloc entre deux increaseProgress() — d'un ordre de grandeur à l'autre, aucune estimation n'en sort. Enfin l'en-tête, qui empruntait .active-sessions-header à un sélecteur descendant d'un autre composant et n'a donc jamais été stylé (#56) : sans display: flex, le bouton tombait à la ligne et me-auto collait le compteur au titre — deux défauts d'apparence, une seule cause.
2026-08-0106262b5 Le glisser-déposer sortant réel, par DownloadURL. L'utilisateur refusait le fonctionnement en deux temps : le téléchargement doit démarrer au dépôt. webContents.startDrag() en est incapable, il exige un fichier déjà sur le disque (#49). La voie retenue passe par le moteur et non par une API d'Electron : une entrée DownloadURL du DataTransfer annonce nom, type et URL, et Chromium ne réclame le contenu qu'au dépôt — le rendu différé de Windows. src/dragOutServer.service.ts sert cette URL en streamant les octets SFTP dans la réponse HTTP, sans copie temporaire : écoute sur 127.0.0.1, port éphémère, jeton aléatoire consommé à la première requête, backpressure respecté. dragstart n'appelle plus preventDefault() pour un fichier ; les dossiers et le cas « serveur non démarré » gardent l'ancien chemin. Validé en test le 2026-08-02 — effet de bord bénin consigné en #55.
2026-08-014a749f1, ae9f3de, abb5cfe Gestionnaire de transferts, clic dans le vide, et mise en page des réglages. Le gestionnaire s'installe en bas de sidebar, masqué quand vide, alimenté par fileTransferStarted$ — la source même de la tab-bar, donc il reflète aussi les transferts du panneau natif et des autres plugins (option (c) de la roadmap). Retirer une ligne active annule le transfert, sémantique reprise du menu natif ; vider demande confirmation. Le clic dans le vide de la liste SFTP désélectionne, sauf sur l'en-tête et sur ... Les réglages reprennent la mise en page de l'autre plugin de l'utilisateur : une .form-line par réglage, le chemin de l'éditeur sous son intitulé et pleine largeur.

Renommer une entrée depuis le panneau SFTP (2026-08-01)

Demande venue en fin de session, livrée sur branche et validée dans la foulée.

DateHashRésumé
2026-08-01e4f0a9d « Renommer... » au clic droit, extension comprise. Le champ porte le nom complet plutôt que la racine : changer l'extension était l'usage demandé, et rien n'avertit en la modifiant — sur un serveur distant, c'est délibéré. Deux garde-fous, tous deux tournés vers ce qui ne peut pas s'annuler : un / est refusé, car rename() accepterait un chemin et déplacerait l'entrée depuis un champ de renommage ; et une collision est vérifiée avant l'appel, SFTP v3 laissant chaque serveur décider — certains écrasent. La sélection suit l'entrée, et la liste se met à jour par la fusion différentielle plutôt que par navigate().

Transferts officiels, fraîcheur des copies, purge et performance de la liste (2026-08-01)

Chantier #transferts-officiels mené sur branche, élargi en cours de route par ce que la passe de test a révélé. Le plan portait sur quatre volets ; trois défauts de fond, tous antérieurs au chantier, ont été trouvés en mesurant.

DateHashRésumé
2026-08-019e7a7c8 Les transferts passent par PlatformService, et le mode est restauré après l'envoi. startDownload/startUpload acceptent un chemin imposé et n'ouvrent alors aucun dialogue (#48) : le transfert maison n'était pas nécessaire, et il coûtait la progression, la file et l'annulation que la tab-bar affiche pour tout transfert publié sur fileTransferStarted$. Les classes maison restent en repli, choisi sur l'arité réelle de la méthode. Découvert au passage : SFTPSession.upload() n'appelle jamais getMode() (#53), donc chaque enregistrement rendait un script 0755 non exécutable, silencieusement, bien avant ce chantier.
2026-08-01aedc8a7 stat() ne rend ni les permissions ni la date — deux corrections fausses avant la bonne. Le mode remis était d'abord celui de la ligne affichée, qui date du dernier readdir : restaurer ça annule un chmod fait au terminal depuis, ce qu'un 755 redevenu 664 a montré. Le mode a donc été lu par stat() — et vaut toujours 0. Une trace en console a tranché en une passe : russh construit son stat() par Object.assign({}, md) sur un objet napi, dont les accesseurs ne sont pas copiés (#50). Sa propre readDirectory() copie champ par champ, avec le commentaire « Can't just spread a napi object ». Portée bien plus large que le chmod : la date revenait en 1970, donc la fraîcheur ne tenait que par la taille, et le garde-fou des liens vers un dossier (#45) testait un mode toujours nul.
2026-08-011a516b8 La purge des temporaires ne s'exécutait pas, et seul un test l'a montré. Le service porte la purge dans son constructeur, mais Angular ne construit pas un service providedIn: 'root' que personne n'injecte (#52) : il n'était atteint que par le panneau SFTP, donc jamais au démarrage — précisément le cas qu'il devait couvrir. Trouvé en semant un résidu avant le lancement plutôt qu'en relisant le code, ce que la relecture n'aurait pas donné : aucune erreur, le service existe, il ne se construit simplement jamais.
2026-08-01576a5d6 · d3695e8 Un dossier très peuplé rendait le panneau inutilisable ; la cause n'était pas le nombre de lignes. C'est ce que chacune refaisait à chaque cycle de détection (#54), et surtout un getter renvoyant un tableau neuf qui alimentait un *ngFor dans chaque ligne. Les lignes sont précalculées et mises en cache. Le rafraîchissement automatique, demandé dans la foulée, applique l'idée de l'utilisateur — garder le cache et ne mettre à jour que la différence : rien ne diffère ⇒ rien n'est réassigné et le cycle ne rend rien ; sinon les entrées inchangées gardent leur objet, donc trackBy conserve les lignes, sans clignotement ni perte du défilement. Il remplace navigate(), qui vidait la liste avant de la remplir.
2026-08-01026dd7b Deux réglages d'ergonomie, mesurés plutôt que supposés. Les notifications étaient jugées trop brèves : NotificationsService.notice() code en dur timeOut: 1000 (#51) — une seconde pour des messages demandant une action. Elles passent par ToastrService, l'instance même de Tabby. 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.

Ctrl+Entrée insère un saut de ligne (2026-08-01)

Premier chantier mené sur une branche depuis que l'option branches vaut branche : code et documentation arrivent ensemble sur master. Parti d'un signalement de régression du plugin, il s'est terminé en fonctionnalité — le diagnostic ayant montré qu'il n'y avait aucune régression à corriger.

DateHashRésumé
2026-08-0102e7d24 Le diagnostic a retourné le problème. Signalé comme une régression imputée à la sélection multiple, mesuré au literal next de readline (Ctrl+V puis la touche) : Entrée et Ctrl+Entrée émettent tous deux ^M, seul Ctrl+J émet LF. Les deux frappes sont donc indistinguables une fois écrites sur le pty : aucune application distante ne peut les séparer, et il n'y avait rien à réparer — ni dans le plugin, ni dans l'application, ni dans Tabby. Trois observations avaient déjà écarté les fausses pistes : symptôme identique en local et en SSH sur des serveurs différents, aucun raccourci Ctrl-Enter déclaré par les trois plugins tiers chargés (vérifié dans leurs bundles), et une version de Tabby inchangée depuis le 2026-07-22.
2026-08-0102e7d24 La correction se place là où la distinction survit encore : le renderer. L'événement clavier y porte ctrlKey, et la couche de raccourcis de Tabby s'exécute avant l'écriture sur le pty. Un HotkeyProvider déclare l'action, le ConfigProvider lui donne Ctrl-Enter par défaut — sans quoi elle apparaîtrait dans les réglages sans aucune touche — et un service abonné à hotkey$ appelle sendInput('\n'). hotkey$ et non unfilteredHotkey$ : le flux filtré se tait quand un champ de saisie a le focus, ce que le test a confirmé depuis le champ de renommage de la sidebar. La cible est le volet focalisé, jamais le split que rend app.activeTab, et l'absence de terminal en façade ne déclenche rien plutôt que de deviner une destination.

Cadrage rouvert sur attribution — purge de l'attribution IA (2026-08-01)

Entretien rouvert sur cette seule clé. attribution passe de oui à non : plus aucune métadonnée d'attribution IA dans les messages de commit, et l'historique existant est purgé plutôt que laissé en l'état. Le motif enregistré au cadrage du 2026-07-31 — « revenir dessus imposerait de réécrire l'historique » — n'a pas été retenu comme suffisant : la réécriture est assumée. La trace de la collaboration reste dans les documents de gouvernance, qui la portent avec leur contexte.

DateHashRésumé
2026-08-01 Réécriture des 74 commits, dont 33 portaient Co-Authored-By et 30 Claude-Session. Conditions vérifiées avant de commencer : historique linéaire (aucune fusion), aucun tag, aucune autre branche, arbre de travail propre — la correspondance ancien→nouveau est donc reconstituable par position, ce qu'un historique ramifié n'aurait pas permis. Contrôle après coup : git diff entre l'ancien et le nouveau sommet est vide — l'arbre est identique à l'octet près, seuls les messages ont changé. Les 73 hashs courts cités dans AI-HISTORY.html et ROADMAP.html ont été reportés dans le même geste, sans quoi tout le journal aurait pointé dans le vide. Deux valeurs de 7 caractères ont été délibérément laissées intactes : 6e52454 et 10ddd25 sont des empreintes de blob de la charte, pas des commits.

Passe de test du glisser-déposer sortant (2026-08-01)

Le chantier glisser-déposer sortant, écrit le 2026-07-30, est resté deux jours sans passe de test. Entre-temps, la campagne de gouvernance a poussé master avec seize commits derrière lui : du code jamais exécuté a séjourné sur un dépôt public, ce que l'option branches tranchée le 2026-07-31 empêche désormais par construction. La passe le valide sans correctif ; elle produit en revanche trois constats d'usage, rassemblés dans un chantier dédié.

DateHashRésumé
2026-08-018a89787 Drag-out validé, et les deux inconnues du chantier levées. Relâcher un glisser au-dessus du panneau SFTP lui-même ne réveille pas son dropZone : pas de ré-upload de la copie, donc pas d'écrasement silencieux — le garde-fou envisagé (drapeau posé au dragstart) est sans objet. Et draggable='true' posé sur la ligne n'a pas mangé le second clic : le double-clic d'ouverture est intact. Reste acquis et assumé : le premier geste ne dépose rien, il prépare la copie — startDrag() exige un fichier local et le dragstart est synchrone (piège #49).
2026-08-018a89787 Trois défauts trouvés à l'usage, dont un de perte de fraîcheur. (1) Une copie locale est resservie quel que soit l'âge du fichier distant : la Map ready de SftpDragOut et la réouverture d'une session dans SftpRemoteEditor n'ont aucune invalidation, alors que SFTPFile porte size et modified. Le symétrique — écrasement d'un distant modifié pendant l'édition — était déjà documenté comme limite assumée : les deux se referment par la même empreinte. (2) Les copies temporaires ne disparaissent qu'au dispose() du panneau, donc jamais après une fermeture brutale. (3) Le téléchargement d'un dossier est lent (26 fichiers / 11 Mo, plus d'une minute) là où le natif est quasi instantané ; un fichier de 10 Mo par double-clic l'étant tout autant, le transfert maison est hors de cause — le diagnostic reste ouvert entre la récursion séquentielle et le gel du renderer pendant le geste.
2026-08-018a89787 Principe de conception posé par l'utilisateur : si un moyen officiel existe, on le réutilise et on l'améliore. Vérification faite dans la foulée sur le bundle installé : startDownload() et startUpload() acceptent un chemin imposé et n'ouvrent alors aucun dialogue, contrairement à ce que déclarent les typings npm et à ce qu'affirmait l'en-tête de sftpLocalTransfer.ts (piège #48). Le transfert maison n'était donc pas nécessaire, et il coûtait la progression, la file et l'annulation que la tab-bar de Tabby affiche déjà pour tout transfert publié sur fileTransferStarted$. Porté en chantier, avec la conséquence à rejuger sur Transferts SFTP avant de le démarrer.

Cadrage de gouvernance du 2026-07-31 — PROFIL.md, validateur et charte 20260731-204511

Session ouverte à la racine du workspace pour relancer l'entretien de cadrage (A-15 Cas B, déclencheur « révision de la charte touchant le noyau »). Périmètre restreint à ce projet par l'utilisateur : tabby-better-vault n'est pas touché et sa branche de propagation reste non fusionnée. Le chantier Conformité de la gouvernance porte les deux items encore ouverts — audit A-15 Cas B, puis mention de conformité du pied de page, dans cet ordre.

Deux réponses de cadrage changent une pratique au lieu d'enregistrer l'existant : branches passe à branche (le projet travaillait en direct sur master) et outillage vaut oui, ce qui rend l'état « Adopté » utilisable — la classe .adopted existait dans style.css sans qu'aucun chantier ne puisse la porter. Trois dérives ont été constatées et corrigées en séance (A-5) ; elles sont décrites dans les entrées ci-dessous.

DateHashRésumé
2026-07-311e39041 Audit A-15 Cas B mené sur les quatre axes, et remise à niveau close. Trois agents en lecture seule ont couvert en parallèle le contexte, la roadmap et l'index ; le journal a été vérifié à part, ne se corrigeant jamais rétroactivement. Contexte : trois pièges corrigés en place, aucun supprimé — #19 décrivait un min-height: 8px inconditionnel disparu du code au profit du correctif :empty de #26 ; #33 supposait qu'on montait SFTPPanelComponent lui-même, alors que le code monte depuis une sous-classe à template propre dont les :host hérités ne s'appliquent jamais ; #5 justifiait --ignore-scripts par tabby-ssh, retiré des dépendances depuis le piège #34 — le flag reste nécessaire pour @parcel/watcher. Roadmap : aucun statut à faire redescendre, les seize « Fait » portant chacun une trace de code et de validation ou de mise en service. Journal : conforme A-4, treize hashes cités pointant tous vers des commits réels. Index : un écart, de moi — ce projet annonçait « 24 options » là où la charte en compte 22, un comptage qui incluait deux valeurs de l'option seuil ; l'erreur était symétrique donc la conclusion tenait, mais le chiffre contredisait PROFIL.md. Dates de dernière revue mises à jour, et le pied de page de README.html passe enfin à « version 20260731-204511 » — en dernier, comme sa règle l'impose.
2026-07-3107362bc Le mot de clôture passe de CLOTURE à MAJ (A-13, changement d'option du PROFIL.md sur demande explicite). Le mot vit à trois endroits — PROFIL.md, le CLAUDE.md du projet et celui du workspace — mis à jour dans le même geste. MAJ est nettement moins distinctif que le précédent, étant l'abréviation usuelle de « mise à jour » : la règle de désambiguïsation gagne un paragraphe dédié dans les deux CLAUDE.md, et la réserve est écrite à côté de la valeur dans le profil pour qu'une relecture du cadrage la retrouve.
2026-07-3171d47c1 Charte 20260731-204511 propagée, REMISE-A-NIVEAU.md supprimé dans le même commit que la fusion. Trois révisions traversées depuis la dernière conformité. Vérifié plutôt que supposé, la branche l'affirmant sans le prouver : 24 options de part et d'autre, aucune ajoutée ni retirée, quinze invariants inchangés — donc aucune question de cadrage nouvelle et un PROFIL.md qui reste valide. Copies contrôlées par empreinte Git (6e52454, 10ddd25), pas par diff. La règle qui fixe la fin de vie de ce fichier est arrivée par le dépôt canonique (révision 20260731-203812) après avoir été constatée ici : voir 4bc798a. La ligne « Charte au moment du cadrage » du profil reste à 20260731-150737 : elle date le cadrage, pas la copie.
2026-07-314bc798a Dérive — la roadmap affichait « ✅ Revue faite (2026-07-29) » pendant qu'une remise à niveau était engagée avec deux items ouverts. Le corps du chantier avait été rafraîchi au commit précédent, la pastille non, et REMISE-A-NIVEAU.md disait l'inverse : deux documents décrivaient le même fait et se contredisaient (A-2). C'est précisément ce que l'audit A-15 Cas B est censé attraper — « un chantier marqué livré dont on ne trouve pas trace de mise en service redescend » —, le mécanisme n'avait simplement pas été appliqué à lui-même. Pastille passée en progress, classe jusqu'ici définie mais employée nulle part ; items rapatriés dans le chantier de roadmap, qui existait déjà ; fichier supprimé après vérification qu'aucun document ne le référençait et que son seul contenu non repris dupliquait l'historique des révisions de la charte.
2026-07-31b5bb425 Références « Règle N » migrées vers les identifiants A-x dans README.html et ROADMAP.html. Les trois occurrences d'AI-HISTORY.html restent en place : A-4 impose un journal en ajout seul, et elles nommaient la règle telle qu'elle existait à la date de l'entrée. Deux dérives corrigées au passage — les deux documents situaient l'original de la charte « à la racine du workspace », alors que c'est un miroir de commodité et non la source, et décrivaient la vérification de conformité comme une comparaison de dates là où la charte est versionnée par identifiant horodaté depuis 20260731-135838.
2026-07-31f496668 Piège #47 et compteur de numérotation. core.autocrlf=true sur ce poste contre le eol=lf du dépôt canonique : un diff nu de la charte rapporte toutes les lignes comme différentes alors que les blobs Git sont identiques — indiscernable d'une copie périmée ou adaptée au projet. Rencontré en vérifiant la propagation, écrit sans feu vert (A-3, le contexte s'écrit au fil de l'eau). AI-CONTEXT.html n'indiquait pas son prochain numéro libre, ce qu'A-6 impose : l'information ne vivait que dans CLAUDE.md, au mauvais endroit et déjà fausse d'un cran. Compteur posé en tête à #48.
2026-07-31945339d Création de PROFIL.md et du validateur de documents. Entretien de cadrage complet, profil de départ complet : les vingt-deux options reçoivent une réponse explicite, aucune ne reste « défaut appliqué, non tranché ». validateur était sans réponse alors qu'A-14 impose de valider — html-validate entre en devDependency avec npm run lint:airules. Deux règles neutralisées (doctype-style, prefer-tbody) : cosmétiques, aucune ne détecte une structure cassée, et les satisfaire imposerait de réécrire en masse des documents dont ce journal. Contrôlé sur un fichier volontairement cassé, close-order est bien remonté.
2026-07-31d8b56b8 Fusion de la branche de veille (504c1f4..d8b56b8, fast-forward) : charte 20260731-150737 et GABARITS.md arrivent verbatim sur master. La fusion est un acte de validation humaine explicite (option veille-conformité) — la tâche planifiée pousse la branche, elle ne fusionne jamais d'elle-même. Le hash cité est le sommet de branche : aucun commit ne représente le fast-forward lui-même.

Campagne UX/sécurité du 2026-07-30 — suppression SFTP et sessions actives

Trois des quatre chantiers demandés le 2026-07-30 (suppression SFTP par Suppr, sessions actives en vue SFTP, éditeur forcé au double-clic), plus l'écart d' audit des confirmations refermé au passage — l'audit n'est pas un livrable mais un invariant à tenir. Menés directement sur master. Les deux premiers commits ont été poussés non testés ; la passe de test de l'utilisateur les a validés et a fait apparaître, sur les sessions actives, un défaut de mise en page que la correction d'origine ne pouvait pas résoudre seule. Reste de la campagne : glisser-déposer sortant.

DateHashRésumé
2026-07-301419073 Le double-clic force un éditeur, jamais l'association du système (#editeur-force). platform.openPath() disparaît : ouvrir par association exécute un binaire, ou un script dont l'extension est liée à un interpréteur, au lieu de l'éditer. L'éditeur est lancé par spawn (detached + stdio: 'ignore' + unref(), sans quoi il meurt avec Tabby ; l'ENOENT d'un éditeur déplacé arrive de façon asynchrone, d'où le listener error), et un échec ne retombe jamais sur l'association — ce serait annuler tout le chantier. L'ouverture est résolue avant le mkdir et le téléchargement : sortir plus tard laissait une copie temporaire, un fs.watch vivant et une session enregistrée pour un fichier que personne n'avait ouvert.
2026-07-301419073 Trois décisions prises pendant le test, dont deux hors du plan initial. (1) Sans éditeur configuré, c'est la fenêtre « Ouvrir avec » de Windows qui s'ouvre — l'utilisateur ne veut pas chercher un .exe dans Program Files. Elle ne rend rien, et sa trace registre est indexée par extension, donc inutilisable pour les fichiers de conf sans extension qui sont le cas d'usage réel : l'application choisie est retrouvée dans la liste des processus, qui porte notre chemin temporaire sur sa ligne de commande (piège #46). (2) Un onglet de réglages plutôt qu'un champ isolé, calqué sur celui de tabby-better-vault (weight = 2, donc juste avant lui) — c'est la surface où poseront les futures options transverses. (3) Une option non prévue : le bouton activé par Entrée dans la confirmation de suppression SFTP. L'utilisateur a d'abord demandé Supprimer pour enchaîner Suppr+Entrée, puis rejugé — le défaut est Annuler, la réponse non destructrice, et l'autre est un opt-in. Appliqué aux deux déclencheurs : la même modale répondant différemment selon la façon dont elle a été ouverte serait un piège, pas une fonctionnalité.
2026-07-301419073 Bug trouvé par le chantier sans lui appartenir : un lien symbolique vers un dossier partait au téléchargement. Signalé en test par « Impossible de télécharger infra », avec un Status Failure — ce qu'OpenSSH renvoie pour EISDIR. En cause, SFTPFile.isDirectory, que SFTPSession remplit depuis metadata.type : le format SFTP v3 ne transporte pas ce champ, un serveur ne répond à SSH_FXP_STAT qu'avec un mot de permissions, donc le stat() de la cible rendait false pour un dossier. Le type est lu dans le mode ((mode & 0o170000) === 0o040000), comme le fait le template natif de Tabby (piège #45). Deux leçons pour la suite : un booléen dérivé d'un champ absent du protocole ment silencieusement, et un Status Failure générique côté russh mérite qu'on remonte au code POSIX qu'il traduit avant de suspecter son propre code.
2026-07-30e054ae6 Trois correctifs sur la section « Sessions actives », issus du test. (1) Placement : retirer le !sftpMode du *ngIf (commit précédent) rendait bien la section visible en vue SFTP, mais elle s'affichait sous le panneau, rejetée en bas de la sidebar — les blocs étaient placés après <sidebar-plus-sftp>, qui est flex-grow-1 dans la colonne flex. L'ordre du template comptait autant que la condition. Leçon : une section « visible en mode X » ne se corrige pas seulement par sa condition d'affichage tant qu'un frère extensible la précède. (2) Compression : .active-sessions n'avait pas de déclaration flex, contrairement à .active-tunnels — donc flex-shrink: 1 par défaut, et le panneau juste en dessous écrasait la boîte sans écraser ses lignes, qui débordaient par-dessus. Passé en flex: 0 0 auto. (3) Plafond : la liste s'arrête à 4 lignes puis défile, sur un wrapper .active-sessions-list pour que l'en-tête et son compteur restent fixes ; le line-height des lignes est figé à 1.25rem parce que la hauteur maximale en dérive — une valeur héritée du thème aurait fait tomber le plafond sur 3,5 ou 4,5 lignes. Deux décisions de l'utilisateur au passage : les tunnels actifs sont masqués en vue SFTP (l'inverse des sessions, l'onglet SFTP n'ayant pas d'usage de l'état de forwarding), et le libellé d'une ligne suit le renommage manuel de l'onglet quand il y en a un — customTitle || topmostParent.customTitle || profil || titre, la précédence de l'en-tête d'onglet natif. Le repli sur topmostParent est structurel : les deux points d'entrée du renommage (double-clic sur l'en-tête, menu contextuel) visent l'onglet de haut niveau, donc pour un pane le customTitle vit sur le SplitTabComponent et jamais sur le pane. Au passage, .active-tunnels était la seule règle du SCSS non scopée à sidebar-plus-tree — elle fuyait en CSS globale dans le reste de Tabby.
2026-07-30dcabce7 Garde-fous sur la suppression SFTP. event.repeat et un drapeau deleteInFlight empêchent le keydown de Suppr de se re-déclencher en boucle pendant que la modale est ouverte — le focus y atterrit sur un bouton et non sur un champ, le filtre existant ne suffisait donc pas. Le retrait de l'entrée « Delete » native se fait désormais par position (CommonSFTPContextMenu est l'unique provider, vérifié) plutôt qu'en testant un libellé traduit : l'app installée n'a pas de catalogue français pour cette clé, le test de texte était inopérant. Il reste comme simple console.warn de diagnostic si un futur Tabby réordonne le menu.
2026-07-30efcf3f8 Suppression SFTP par Suppr et sessions actives en vue SFTP. Nouveau ConfirmModalComponent (NgbModal) partagé par toutes les confirmations de suppression SFTP : la touche Suppr sur l'entrée sélectionnée de la liste distante, et l'entrée « Delete » du clic droit — celle-ci, héritée telle quelle de SFTPPanelComponent, confirmait par platform.showMessageBox(), un vrai dialogue OS (piège #42), en contradiction avec l'invariant de confirmation en HTML. Suppression récursive pour un dossier. Retrait de la condition !sftpMode sur les sections « sessions actives » et « tunnels actifs » — correction incomplète, voir e054ae6 ci-dessus.

Revue de gouvernance — mise en conformité à la charte (2026-07-29)

Revue au sens de la charte, Règle 7 Cas B : audit d'un .AIRules/ déjà au format mais qui a dérivé. Déclenchée en cours de session ordinaire, après un rappel de l'utilisateur sur une règle que je n'appliquais pas — les fichiers de .AIRules/ se commitent et se poussent à chaque mise à jour, ce qui était déjà écrit dans la charte (Règle 1) et resté lettre morte. Cette entrée acte la revue et les écarts constatés.

DateHashRésumé
2026-07-29df8ef82 Quatre écarts corrigés, tous antérieurs à la révision de charte du 2026-07-28. (1) L'intro de README.html annonçait ce dossier « hors du dépôt Git » — faux, et contredit par le paragraphe suivant de la même page ; le chemin cité (.AIRules/tabby-better-sidebar/) n'existait pas davantage. (2) Les cadences d'écriture disaient d'alimenter AI-HISTORY à chaque commit et ROADMAP à chaque changement de statut, là où la charte exige depuis le 2026-07-28 le feu vert explicite de l'utilisateur pour ces deux pages, AI-CONTEXT restant la seule alimentée proactivement. (3) Le pied de page ne portait pas la mention « Conforme à la charte du … », qui est précisément le marqueur qu'une session ultérieure compare pour détecter qu'un projet est resté sur une convention antérieure. (4) La page affirmait l'existence d'un CLAUDE.md à la racine de Développement/ — créé à cette occasion, il n'existait pas.

La charte voyage désormais avec le projet : GOUVERNANCE-IA.md est copié à l'identique dans ce .AIRules/, la racine du workspace n'étant pas un dépôt Git — cette copie versionnée en est la seule sauvegarde. Copie conforme, jamais adaptée au projet, pour rester comparable à l'original par un diff ; à repropager à chaque révision de la charte.

CLAUDE.md enregistre enfin la décision par dépôt qu'exige la Règle 3 sur les métadonnées d'attribution IA dans les messages de commit : admises — ce qui enregistre la pratique déjà présente dans l'historique public plutôt que de choisir à neuf, revenir dessus imposant une réécriture d'historique.
2026-07-29e6d8136 Chantier « Transferts SFTP » ajouté à la roadmap à la demande de l'utilisateur, en priorité Haute, devant Auto-Enter : gestionnaire de transferts horodaté en bas de sidebar masqué quand vide, dépôt depuis l'OS directement sur un dossier distant, glisser-déposer interne au panneau SFTP. Rien n'est implémenté. La section consigne surtout la vérification d'API préalable, faite sur le Tabby installé : la directive dropZone n'arrête pas la propagation et ne transmet pas le DragEvent ; FileTransfer n'expose aucun signal de fin ; startUploadFromDragEvent() publie chaque fichier dans fileTransferStarted$, ce qui fait ouvrir le menu de transferts natif à chaque dépôt ; et nos propres transferts, ne passant pas par PlatformService, n'atteignent aucune liste. Deux points tranchés par l'utilisateur : lignes sobres plutôt que les barres de progression du menu natif, et — sur sa question de réutiliser ce menu — la vérification que TransfersMenuComponent n'est ni dans les exports du NgModule de tabby-core ni dans le bloc d'export du bundle compilé, sa source de données restant réutilisable là où son composant ne l'est pas.

Tunnels SSH (2026-07-29)

Deuxième des trois chantiers de priorité moyenne attaqués d'affilée. Ouvert par une note de faisabilité avant tout code, sur le paquet tabby-ssh installé et non celui de node_modules (piège #32), aux deux niveaux typings + bundle. C'est cette vérification qui a délimité le chantier : la lecture de l'état et l'écriture en configuration sont acquises, l'action sur une session vivante ne l'est pas.

DateHashRésumé
2026-07-29c4422f6 Tunnels SSH : popup « Tunnels du profil… » (liste, ajout, suppression, modification par double-clic), pastille à deux états sur les profils — allumée si des tunnels sont montés, estompée s'ils sont seulement configurés — et section « Tunnels actifs » repliable montrant la description de chaque tunnel, avec ouverture dans le navigateur (PlatformService.openExternal()) et saut vers la session.

Périmètre restreint en cours de route. Une entrée « Gérer les tunnels… » déléguant à SSHTabComponent.showPortForwarding() a été livrée et validée, puis retirée à la demande de l'utilisateur (« je veux uniquement ma solution maison »). Il en découle que rien ne peut agir sur une session ouverte, et que ce n'est pas contournable — addPortForward() exige un ForwardedPort, classe non exportée à l'exécution. D'où le toast de rappel après enregistrement et le verrou de suppression sur les seuls tunnels réellement montés (les autres se suppriment librement — première version bloquant tout dès qu'une session existait, corrigée sur retour de test).

Deux détails tranchés en test : le toast passe par notifications.info() et non notice(), ce dernier codant timeOut: 1000 en dur dans tabby-core — une seconde pour lire une phrase d'explication ; et les boutons de la section restent affichés en permanence, en flux, après une première version en survol qui recouvrait le nom de la session.

Sélection multiple (2026-07-29)

Premier des trois chantiers de priorité moyenne. Mené sur master, avec la répartition désormais habituelle : l'assistant pilote le cycle de vie de Tabby (--debug --remote-debugging-port=9333) et lit la console via CDP, l'utilisateur fait les gestes d'interface et valide point par point.

DateHashRésumé
2026-07-29a0bca09 Trois pièges consignés dans AI-CONTEXT : #39 (attr.x sans crochets en pug n'est pas un binding), #40 (masquer des lignes pendant un glisser CDK casse le pointage et le cache), #41 (la reprise après await config.save() sort de la zone Angular).
2026-07-29bcc92b8 Sélection multiple et déplacement en lot : clic simple/Ctrl/Shift, déplacement par glisser ou par le menu du dossier cible, barre de sélection en bas de sidebar faisant aussi office de confirmation. Le multi-drag, écarté en roadmap comme trop fragile, est devenu trivial une fois moveSelectionToGroup() écrit et testé pour le menu.

Trois cycles de correction ont porté sur le seul placement du lot, tous parce que CdkDragDrop.currentIndex ne veut pas dire ce qu'il semble dire (il s'exprime contre le tableau privé de la seule ligne suivie par CDK). Ce qui a tranché est une trace de cinq lignes imprimant l'index, la ligne survolée et le tableau avant/après — après quoi le placement est passé sur une mesure directe des lignes rendues. Leçon déjà écrite au piège #30, re-vérifiée ici : sur un bug de placement, instrumenter avant de raisonner.

Section « Sessions actives » (2026-07-29)

Premier chantier de la liste restante, mené directement sur master. Même répartition que le chantier précédent : l'assistant lançait Tabby en --debug --remote-debugging-port=9333 et lisait la console via CDP, l'utilisateur faisait les gestes d'interface et validait point par point.

DateHashRésumé
2026-07-298b3dcfa La section elle-même, en haut de la sidebar, repliable, masquée quand rien n'est connecté. Une ligne par pane et non par onglet : un split de trois sessions donne trois lignes, « aller à l'onglet » n'aurait pas eu de sens. Clic = focus, action de survol pour ouvrir le SFTP de cette session. La vivacité exige deux conditions — d'abord testée sur le seul sshSession.open, elle laissait les sessions terminées listées indéfiniment après un exit (piège #37 : le transport survit au shell). La liste se reconstruit sur le sondage de statut existant à 2 s, seul mécanisme qui rattrape une coupure côté serveur, mais le tableau n'est réaffecté que s'il a réellement changé : le réassigner deux fois par seconde reconstruisait chaque ligne et faisait perdre le :hover dont dépend la superposition des actions — le bouton SFTP disparaissait sous le curseur. État replié en localStorage plutôt qu'en clé sidebarPlus.* (rien à synchroniser entre machines, et le piège #16 est contourné plutôt que respecté). Les boutons d'action sont des div, jamais des <a> (piège #38).
2026-07-29a652f0f Socle partagé et correctif du suivi de focus. getAllOpenTabs() était dupliqué mot pour mot entre l'arbre et le panneau SFTP, et la liste des sessions en devenait le troisième consommateur → src/tabs.ts, avec findParentSplit() et focusTab() (sélectionner le split puis déplacer le focus dedans ; selectTab() seul ramène le split sur le pane qu'il avait en dernier). Corrige au passage un bug préexistant du panneau SFTP : watchSplitFocus() gardait son abonnement dès qu'il en avait un, donc après un passage du split A au split B il continuait d'écouter A pour toujours. Invisible jusqu'ici parce que sélectionner un split ne fixe pas en soi le pane focalisé — seul un changement de focus ultérieur dans B aurait corrigé, et il n'arrivait jamais. Le nouveau raccourci « ouvrir le SFTP de cette session » emprunte exactement ce chemin et aurait lié le panneau au mauvais pane.

SFTP contextuel dans la sidebar (2026-07-29)

Chantier « SFTP contextuel », premier de la roadmap en priorité absolue. Mené sur la branche feat/sftp-panel, en aller-retours serrés avec l'utilisateur : l'assistant pilotait Tabby (lancement en --debug --remote-debugging-port=9333, lecture passive de la console via CDP, mesures du DOM rendu), l'utilisateur faisait les actions d'interface et validait. Design d'origine dans ROADMAP § SFTP contextuel.

Question tranchée d'entrée : SCP plutôt que SFTP ?

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. Faire du SCP imposerait de réimplémenter le protocole sur un canal exec brut. Et l'avantage supposé ne s'applique pas : SCP ne gagne que sur le streaming d'un gros fichier unique, et n'a ni readdir, ni stat, ni rename — soit exactement le métier d'un navigateur de fichiers.

DateHashRésumé
2026-07-29c3ea59c Colonnes et options d'affichage dans un menu d'en-tête, façon SFTP+. Clic droit sur les titres de colonnes → réglages directement, au lieu d'un menu générique menant à « Paramètres d'affichage » ; le sélecteur en ligne et son bouton de barre d'outils, devenus un second chemin vers la même chose, sont supprimés. Agencement repris du leur : colonne Nom d'abord et cochée en permanence, puis les colonnes optionnelles, puis quatre options nouvelles (sftpFoldersFirst, sftpShowHidden, sftpColumnBorders, sftpZebra, toutes déclarées dans les defaults, piège #16). Bordures et zébrures deviennent optionnelles, pilotées par une classe sur la grille. La liste est désormais triée (locale, insensible à la casse, dossiers d'abord au choix) — readdir renvoyant l'ordre du serveur, rarement lisible. displayedFiles est un getter au-dessus d'un champ privé hérité, donc évalué à chaque cycle de détection : mémoïsé sur l'identité de filteredFileList, que le parent réaffecte toujours au lieu de la muter (vérifié dans le bundle avant de s'y fier).
2026-07-29e8a0024 Menu contextuel sur le vide de la liste : créer un dossier, créer un fichier, paramètres d'affichage. L'en-tête est volontairement inclus dans la zone déclenchante — ce n'est ni un fichier ni un dossier. Créer un fichier passe par SFTPSession.upload() avec un transfert de zéro octet : la session n'a pas de « créer un fichier » et open() exigerait les drapeaux OPEN_WRITE/OPEN_CREATE de russh, module natif inatteignable depuis ce plugin. Nom vérifié contre un stat() préalable, créer par-dessus aurait tronqué silencieusement. clampInViewport() sort de SidebarPlusTreeComponent vers src/viewport.ts : c'était une méthode private static et le navigateur SFTP en avait besoin aussi — une seconde copie est précisément ce que le point unique du piège #30 cherche à éviter.
2026-07-295af7c12 Suppression de la friction à l'ouverture d'un fichier. PlatformService.openExternal() ne transmet une URL directement à l'OS que pour les schémas http/https/ftp/mailto ; tout le reste, file: compris, passe par confirmAndOpenExternal() et son avertissement modal. → openPath(), qui appelle shell.openPath() sans contrôle de schéma ni invite, et qui est déclarée sur le service abstrait dans les deux jeux de typings. Reste le sélecteur « Comment ouvrir ce fichier ? » de Windows pour une extension sans association : celui-là vient de l'OS, aucune API ne l'évite.
2026-07-295551483 Clic = sélection, double-clic = ouverture, et édition distante en place. Un dossier navigue ; un fichier est copié dans un dossier temporaire privé, confié à l'application par défaut, puis surveillé — chaque enregistrement le renvoie au serveur, avec notification systématique (ça écrit sur un serveur vivant, rien ne doit être silencieux). La surveillance porte sur le dossier et non le fichier : beaucoup d'éditeurs enregistrent en écrivant un nouveau fichier puis en le renommant par-dessus, ce qui casse une surveillance posée sur l'inode d'origine. Événements amortis à 400 ms, upload ignoré si ni taille ni mtime n'ont bougé, enregistrements concurrents mis en file. LocalFileDownload/LocalFileUpload (src/sftpLocalTransfer.ts) parce que les seules implémentations livrées par Tabby viennent de startDownload()/startUpload(), qui ouvrent toutes deux un sélecteur de fichiers. L'upload passe par SFTPSession.upload() plutôt que d'écrire directement : il écrit dans <chemin>.tabby-upload puis renomme, donc un transfert interrompu ne tronque jamais la cible.
2026-07-292d06daf Refonte visuelle inspirée de tabby-sftp-plus, dont le design a été lu directement dans la copie installée chez l'utilisateur. Repris : en-tête collant en couleur d'accent sous un filet de 2px (leur signature), survol en teinte d'accent via color-mix plutôt qu'un gris, zébrures, séparateurs de colonnes rendus continus par align-self: stretch, ellipse sur toutes les cellules. Trois écarts assumés : la colonne Taille reste alignée à droite (alignée à gauche dans 60px, on ne compare plus les ordres de grandeur), pas de contain: inline-size (nécessaire chez eux car leurs lignes sont en width: max-content, inutile avec des pistes fixes), pas de barre de résumé (la leur affiche une sélection multiple absente ici). Bordure et arrondi déplacés sur .sftp-body, l'élément qui défile lui-même — un cadre l'enveloppant aurait eu besoin de son propre overflow et serait devenu l'ancêtre découpant de l'en-tête collant.
2026-07-294460ab1 Deux bugs d'affichage de Tabby contournés, révélés en comparant les nouvelles colonnes entre elles. getModeString() est faux sous Windows (piège #35) : il masque item.mode contre des constantes Node que Windows ne définit pas, d'où drw------- pour un dossier 775 — c'est la colonne octale, calculée localement, qui a trahi l'incohérence. Et les dates sortaient en 7/23/2026 dans une interface française, la locale JS par défaut d'Electron étant en-US quelles que soient celles de Windows et de Tabby (piège #36) — au-delà du style, jour et mois s'inversent à la lecture. → longMode() maison et LocaleService.getLocale().
2026-07-2985754d8 En-tête de colonnes collant et colonnes configurables (size, date, mode par défaut, plus modeLong, type, ext), persistées dans sidebarPlus.sftpColumns. Fichiers cachés grisés plutôt que filtrés — aucun interrupteur ne permettait alors de les récupérer. L'édition manuelle du chemin existait déjà (double-clic sur le fil d'Ariane) mais rien ne l'annonçait : bouton crayon ajouté. Pas de colonne propriétaire/groupe possible : SFTPFile ne porte que name, fullPath, isDirectory, isSymlink, mode, size, modified.
2026-07-296c5c74d Sous-classement de SFTPPanelComponent pour posséder le rendu des lignes. Les demandes de l'utilisateur (date sans horodatage, permissions octales, colonnes toujours alignées, infobulles, grille visible) sont hors d'atteinte d'une feuille de style : le template natif rend la date par le pipe tabbyDate, les permissions par getModeString(), ne porte aucun title, et met un *ngIf sur sa cellule taille — un dossier a donc une cellule de moins qu'un fichier et les colonnes ne peuvent pas s'aligner. Patcher ce DOM aurait été écrasé au cycle de détection suivant. Plutôt que tout réimplémenter au-dessus de SFTPSession, seule la template est redéfinie : navigation, ouverture, transferts, menu contextuel, filtre restent hérités. Le glisser-déposer d'upload survit aussi, DropZoneDirective étant exportée par TabbyCoreModule.
2026-07-29a22f2eb Dépendance tabby-ssh supprimée, typings vendorisés — correctif du piège #34, découvert en cherchant pourquoi le panneau restait sur « en attente d'une session SSH active » alors qu'une session était ouverte. Le chargeur de plugins de Tabby ne met en cache que tabby-core, tabby-local, tabby-settings et tabby-terminal ; tout autre tabby-* présent dans le node_modules du plugin est chargé une seconde fois. D'où un instanceof toujours faux contre une classe homonyme, et — plus grave car silencieux — un SFTPPanelComponent appartenant à un SSHModule jamais bootstrappé par Angular. Typings recopiés depuis l'app installée dans src/types/tabby-ssh/ et mappés par paths : sans copie locale, la résolution retombe sur builtin-plugins/, et on compile contre ce qui tourne (ce qui règle aussi le piège #32). Bug propre au dev — un utilisateur installant depuis npm n'a pas les devDependencies.
2026-07-29c2732e5 Correction du piège #33, que j'avais d'abord écrit à l'envers, et élagage des onglets fermés déplacé sur tabClosed$/tabRemoved$ au lieu d'un tick d'une seconde perpétuel. Les composants de Tabby passent leur SCSS par styles: [...] avec l'encapsulation Angular : leur CSS ne fuit donc pas sur une instance greffée. Le vrai piège est le miroir — le panneau garde sa mise en page interne mais arrive sans aucune taille, le dimensionnement étant le travail de son hôte d'origine.
2026-07-29c3ee956 Première tranche : le panneau SFTP natif hébergé dans la sidebar, lié à l'onglet SSH qui a le focus. Monté par createComponent() : SSHModule déclare SFTPPanelComponent mais ne l'exporte pas, son sélecteur est donc invisible depuis le template d'un autre module. Un panneau mis en cache par onglet plutôt que reconstruit : ngOnInit() appelle openSFTP(), qui ouvre un canal neuf à chaque fois, et SFTPSession n'a pas de close() — un canal ne meurt qu'avec sa session SSH. Le cache donne aussi gratuitement la mémoire de navigation par onglet. Même raison pour [hidden] plutôt que *ngIf, avec un [active] empêchant d'ouvrir des canaux pour une vue que personne ne regarde. getFocusedTab() est indispensable : Tabby enveloppe systématiquement les onglets dans un SplitTabComponent.

Favoris d'icônes (2026-07-29)

Chantier « Favoris d'icônes » de la roadmap, enchaîné après la validation des Workspaces. Deux allers-retours avec l'utilisateur en test manuel, chacun ayant fait évoluer le design initial — détail dans ROADMAP § Favoris d'icônes.

DateHashRésumé
2026-07-2976085d5 Clé sidebarPlus.favoriteIcons (déclarée dans les defaults, piège #16), section « Favoris » dans le picker, épinglage par clic droit sur n'importe quelle tuile. Le design d'origine prévoyait un bouton étoile sur la tuile : implémenté d'abord, puis abandonné parce qu'il ressortait rogné — les grilles du picker ont un overflow-y qui découpe tout débordement de tuile. Le menu est donc rendu hors du picker, avec son propre état d'ouverture et ses propres coordonnées (contextMenuMode aurait démonté le picker, contextMenuX/Y l'aurait déplacé), et affiche l'icône concernée plutôt qu'une étoile générique. Recadrage mutualisé via clampInViewport() pour rester sur le point unique du piège #30. Section « Favoris » gardée visible même vide, avec l'indication du geste. Persistance vérifiée sur disque avant redémarrage, puis confirmée par l'utilisateur après relance complète.

Test manuel des Workspaces : 5 correctifs (2026-07-29)

Passe de test manuel du chantier Workspaces conduite par l'utilisateur (l'assistant en écoute passive : logs du renderer via CDP, lecture directe de config.yaml — déchiffré pour l'occasion, le coffre le rendant sinon illisible depuis l'extérieur de l'app). Trois retours, dont deux ont révélé des bugs de fond ; validé par l'utilisateur en fin de passe.

DateHashRésumé
2026-07-2942dcb09 Popups jamais recadrées à l'écran quand elles s'ouvrent depuis un item de menu (piège #30). Une première explication — plausible, prédisant le bon symptôme, mais fausse — a coûté un cycle de correction : la séquence d'événements (contextmenu suivi d'un mouseup relançant un cycle, contre click dernier de sa séquence). Ce qui a tranché : une trace temporaire imprimant l'élément réellement mesuré et sa hauteur, qui a donné mode=createGroup elt=group-context-menu h=74 alors que la popup rendue faisait 109px. Les deux branches *ngIf basculent dans le même cycle et le DOM lu depuis ngAfterViewChecked() contient encore l'élément sortant. Correctif : mesure différée d'un tour de boucle d'événements. Vérifié par mesure et non à l'œil — top passé de 1026 à 991,25px pour une popup de 109px dans un viewport de 1104, soit exactement la marge de 4px.
2026-07-2942dcb09 Ordre des frères persisté avec l'id périmé après un re-parentage (piège #31), plus deux conséquences en cascade : le repli sur l'ordre de « Tous » était conditionné à la liste entière au lieu de l'être élément par élément, et l'ordre du conteneur d'origine n'était jamais réécrit quand un dossier le quittait. Diagnostiqué sur la vraie config de l'utilisateur, qui portait 526eac40: [a776a5b3] — un id n'existant dans aucun groupe.
2026-07-2942dcb09 Élagage automatique des ids morts dans toutes les listes d'ordre, à chaque écriture d'ordre (option retenue par l'utilisateur parmi trois : nettoyage ponctuel, automatique, ou statu quo). Vérifié sur ses données réelles : les 5 ids morts présents ont disparu au premier glisser-déposer, sans perte — 9 groupes, 25 profils, 0 orphelin avant comme après.
2026-07-29n/a — pas un bug Points 6 et 7 du protocole de test (« la réorganisation faite dans un workspace s'applique aussi aux autres ») : conforme à la conception après vérification du geste réellement effectué — le dossier avait changé de parent, ce qui modifie l'arborescence réelle, donc tous les workspaces. Seuls la visibilité et l'ordre entre frères sont cloisonnés. L'ambiguïté du geste (moitié haute d'une ligne = réordonner, moitié basse = imbriquer) est consignée en roadmap comme point d'ergonomie à traiter.

Workspaces + déblocage complet du glisser-déposer de dossiers (2026-07-28)

Chantier Workspaces de la roadmap, puis une longue chasse au bug : imbriquer un dossier dans un autre était impossible, et l'est resté à travers plusieurs correctifs successifs — il y avait trois causes indépendantes, chacune suffisante à elle seule. Détail technique dans les pièges #22 à #29.

DateHashRésumé
2026-07-2842dcb09 Rangement de l'arborescence réelle de l'utilisateur, à sa demande et après validation de la structure voulue : Maison à la racine, Travail devenu parent de DATACENTER, Sites, HOSTS et Services. Appliqué par glisser-déposer réel piloté via CDP (pas par édition de fichier) — ce qui a servi de recette grandeur nature du correctif. Intégrité vérifiée après coup : 22 profils avant / 22 après, aucun perdu, aucune référence de groupe orpheline, ids migrés correctement dans les deux workspaces existants.
2026-07-2842dcb09 Incident — config.yaml de l'utilisateur corrompu puis réparé. Des éditions scriptées via Get-Content/Set-Content -Encoding utf8 (PowerShell 5.1) ont ré-encodé un chemin de clé SSH accentué (…\Développement\……\Développement\…) et ajouté un BOM. Totalement silencieux : détecté uniquement par un diff contre la sauvegarde, réparé par restauration intégrale, vérifié (accents intacts, pas de BOM). Documenté en piège #27 et en mémoire longue durée — toute édition scriptée d'un fichier accentué passe désormais par Node.
2026-07-2842dcb09 3e et vraie cause racine — le placeholder de CDK (piège #28). Après les deux correctifs précédents, l'imbrication fonctionnait en glissant vers le haut mais jamais vers le bas, à n'importe quelle distance. Quand CDK entre dans la liste cible il y déplace son placeholder, qui libère en partant la place qu'il tenait au-dessus de la cible : celle-ci remonte d'une hauteur de ligne, sort de sous le curseur, CDK ressort, le placeholder revient → oscillation infinie. Vers le haut le placeholder est sous la cible, qui ne bouge pas — d'où l'asymétrie. Signature qui a trahi le mécanisme : la boucle de visée automatique alternait indéfiniment entre deux positions distantes d'exactement 29px. Le tri de CDK a été écarté comme cause en le désactivant (cdkDropListSortingDisabled) sans effet. Correctif : .cdk-drag-placeholder { height: 0 } — et surtout pas display: none, testé d'abord, qui supprime bien l'oscillation mais empêche cdkDropListDropped d'être émis (CDK anime la prévisualisation vers la boîte du placeholder ; sans boîte, la séquence ne se termine jamais). Ajouté en complément : rescueTargetGroupId(), un test de collision maison sur la position réelle des lignes qui rattrape les dépôts résolus à tort sur la racine — dont la mise au point a exigé de découvrir que cdkDragEnded est émis avant cdkDropListDropped (piège #29).
2026-07-2842dcb09 Ergonomie du dépôt, sans ajouter d'espace (piège #26). Contrainte posée par l'utilisateur en cours de route : resserrer l'espacement entre dossiers, donc impossible de rembourrer les zones de dépôt. Retenu : la liste d'enfants, quand elle est vide, est agrandie à 16px puis remontée par une marge négative de même valeur — contribution nette nulle au flux, mais la boîte recouvre la moitié basse de la ligne du dossier. D'où le geste final : moitié basse = « déposer dedans », moitié haute = « réordonner à côté ». L'espacement passe de 45px à 29px par dossier replié. pointer-events: none au repos empêche l'overlay de voler les clics de la ligne. Pistes écartées et pourquoi : ::after absolu (invisible pour getBoundingClientRect, donc rejeté par CDK), marge négative permanente sans :empty (écrase les lignes des dossiers non vides). Surbrillances de zone de dépôt retirées ensuite sur demande de l'utilisateur.
2026-07-2842dcb09 2 premières causes du blocage (pièges #24 et #25), trouvées après que le test manuel eut échoué à départager les hypothèses — d'où la mise en place d'un banc de test qui pilote le glisser-déposer via CDP (Input.dispatchMouseEvent) et mesure la géométrie en direct, sur des dossiers jetables grp-zzz-test-*. (1) L'ordre de cdkDropListConnectedTo : CDK retient le premier conteneur connecté qui matche (.find()) et son test se termine par nativeElement.contains(elementFromPoint), donc groups-root — qui contient dans le DOM toutes les listes imbriquées et figurait en tête — gagnait systématiquement (mesuré : containerId === "groups-root" sur 7 tentatives sur 7). Correctif : tri par profondeur décroissante, groups-root en dernier. (2) Le correctif CSS de la passe précédente (min-height agrandi sur .cdk-drop-list-receiving) aggravait le problème : CDK marque toutes les listes connectées d'un coup, donc l'arbre se décalait de ~90px au démarrage du glisser, et CDK met de toute façon les rectangles en cache avant d'appliquer cette classe.

Suite — fond des popups, distinct du fond de la sidebar (2026-07-28)

DateHashRésumé
2026-07-2807ba909 Retour utilisateur : les menus contextuels/popups (menu Groupe/Profil, picker d'icônes, create-popup) étaient difficiles à distinguer de la sidebar derrière eux. Cause : les deux utilisaient exactement la même variable --theme-bg-more-2. Changé pour --theme-bg (fond de base de l'appli/zone terminal) sur les trois — reste piloté par le thème actif, aucune couleur en dur.

Menu contextuel enrichi — favoris, renommer, lancer (2026-07-28)

Premier chantier de la phase "extension du socle" de la roadmap, design déjà fourni par l'utilisateur. Trois actions ajoutées aux menus contextuels Profil/Groupe existants.

DateHashRésumé
2026-07-283c60f32 Trois retours utilisateur après le test manuel du menu contextuel enrichi, traités différemment selon leur nature. (1) Corrigé immédiatement, bug concret affectant tous les popups existants (pas seulement les nouveaux) : clic droit près du bord bas/droit de la sidebar → menu partiellement rendu sous la barre des tâches, inutilisable. Fix : ngAfterViewChecked() + flag menuPositionDirty, mesure la taille réelle du popup rendu (getBoundingClientRect()) et clampe contextMenuX/contextMenuY pour rester dans le viewport — posé sur chaque point d'ouverture/changement de mode (menu, icon picker, rename, create, confirm delete), pas seulement les trois handlers de clic droit initiaux, car chaque popup a une taille différente. (2) Fait immédiatement, changement trivial : MAX_RECENT_ICONS 5 → 20 icônes "Récentes" affichées dans le picker, demande directe de l'utilisateur. (3) Pas implémenté tout de suite : favoris d'icônes (liste séparée épinglable, distincte de "Récentes") ajouté à ROADMAP.html comme nouveau chantier proposé plutôt qu'improvisé en cours de route — nouvelle UI/clé de config, pas un simple réglage. (4) Ajouté juste après, retour utilisateur supplémentaire sur la même session de test : un en-tête (icône + nom de l'élément ciblé) en haut des menus contextuels Groupe/Profil (.group-context-menu-header, séparé du reste par une bordure), pour lever l'ambiguïté sur quel élément a été clic-droit — absent du menu racine (clic droit sur l'espace vide), qui ne cible aucun élément précis.
2026-07-28n/a (docs, ce commit) Décision explicitement confirmée par l'utilisateur : un groupe favori garde uniquement son étoile dans l'arbre, il ne remonte pas dans le dossier virtuel "Épinglés" (qui reste une liste plate de profils) — perimètre assumé, pas un oubli, laissé tel quel pour l'instant.
2026-07-28cd90b8f Bascule favoris étendue aux groupes (clé sidebarPlus.favoriteGroups séparée de favorites, pour éviter tout risque de collision d'ID entre profils et groupes — déclarée dans les defaults du ConfigProvider, piège #16), renommage inline (mode 'rename', écriture writeProfileGroup({id, name}) pour un groupe à n'importe quelle profondeur — confirmé que writeProfileGroup() fait une recherche flate sur config.store.groups qui stocke TOUS les groupes au même niveau quel que soit leur imbrication visuelle, donc le "racine uniquement" du piège #12 ne s'applique qu'au re-parentage, pas aux écritures de champs simples), et lancement de session (profil : réutilise launchProfile() existant ; groupe : lance ses profils directs uniquement, sans récursion — décision actée avec l'utilisateur de garder ça séparé du futur chantier "Group Exec" plutôt que de fusionner les deux). Bug trouvé par l'utilisateur en testant manuellement sur la jonction de dev avant tout commit : le badge favori de groupe s'affichait sous le nom au lieu d'à côté, décalant toute la suite de l'arbre — piège d'indentation pug (nouveau piège #21 dans AI-CONTEXT.html), corrigé avant que ce commit ne soit fait, donc la version committée est déjà propre.

Diagnostic — jonction NTFS de dev manquante, plugin invisible (2026-07-26)

Signalé par l'utilisateur : le plugin n'apparaissait plus dans la liste des plugins Tabby.

DateHashRésumé
2026-07-26n/a (hors dépôt) Diagnostic : %APPDATA%\tabby\plugins\node_modules\ ne contenait plus tabby-better-sidebar (seul tabby-sftp-plus présent) — la jonction NTFS de dev avait disparu (piège #1, probablement supprimée en désactivant le plugin depuis l'UI Extensions). dist/index.js toujours présent et à jour. Jonction recréée. Rappel donné à l'utilisateur : redémarrer tout le process Tabby.exe, pas un simple reload.

Distinct : classement de recherche npm pour un paquet neuf (2026-07-26)

L'utilisateur cherchait le plugin depuis la recherche npm intégrée de Tabby, pas depuis le mode dev — confusion initiale avec le chantier ci-dessus, deux causes différentes.

DateHashRésumé
2026-07-26n/a (recherche externe) Vérifié directement sur l'API de recherche npm : le paquet est bien indexé (présent pour le mot-clé tabby-plugin) mais absent des 250 premiers résultats même pour une recherche par nom exact — l'algorithme de registry.npmjs.org pondère fortement la popularité (téléchargements/maintenance), inexistante pour un paquet publié quelques heures plus tôt. Pas un bug de publication. Documenté comme piège #20 dans AI-CONTEXT.html, avec le contournement (npm install tabby-better-sidebar direct dans le dossier de plugins).

Restructuration de la documentation — split roadmap → gouvernance IA (2026-07-26)

La roadmap unique (tabby_sidebar_roadmap.md) avait grossi au-delà de son rôle (historique + pièges + roadmap + points fragiles mélangés). Reprend le format déjà utilisé sur un autre projet de l'utilisateur (Travail, dossier .AIRules/) : README (index/protocole), AI-CONTEXT (invariants), AI-HISTORY (journal), ROADMAP (statut/priorités) — converti en HTML statique avec navigation commune.

DateHashRésumé
2026-07-2642dcb09 Ancien tabby_sidebar_roadmap.md retiré, contenu porté (condensé, doublons et narration de délibération retirés) dans .AIRules/{README,AI-CONTEXT,AI-HISTORY,ROADMAP}.html. Numérotation des 20 pièges et des 7 points fragiles conservée à l'identique (référencée depuis CLAUDE.md). Dossier positionné dans le dépôt tabby-ssh-sidebar (pas au niveau parent Développement comme envisagé un temps) pour rester suivi par Git avec le code.

Révisions de roadmap suite à consultations externes (Gemini) (2026-07-26)

Trois échanges successifs avec Gemini (Google) pour challenger la roadmap depuis une perspective dev/sysadmin/gestionnaire, plus une clarification directe de l'utilisateur.

DateHashRésumé
2026-07-265377ef1 Décisions finales (3e échange) : Vault Auto-Unlock sorti du périmètre de better-sidebar, prévu comme plugin séparé avec interfaçage optionnel via l'injecteur Angular partagé — clarifié par l'utilisateur que l'objectif de "portabilité" derrière le stockage JSON externe des workspaces était en fait le transport de sa propre config entre machines, pas le partage d'équipe : stockage des workspaces ramené sur config.yaml (annule la proposition JSON externe du 2e lot), export/import ponctuel ajouté pour le partage sélectif à la place. Précisions confirmées : couleurs contextuelles par workspace (structuredClone au double-clic), UI adaptative via ResizeObserver, sécurité RAM du futur plugin Vault (purge immédiate du mot de passe déchiffré après injection).
2026-07-26(inclus dans 5377ef1) 2e lot Gemini : précisions sur le SFTP (liaison au focus split-pane avec option de gel, lazy loading anti-freeze, suivi des transferts par flèches d'activité plutôt que barre de progression), sur les Workspaces (à ce stade encore stockage JSON externe + favoris cloisonnés + logique de cascade, UI onglets→dropdown), sur la sélection multiple (comportements calqués sur les standards OS), sur les tunnels SSH (surcouche du moteur natif Tabby), et proposition initiale du Vault Auto-Unlock avec purge RAM.
2026-07-26(inclus dans 5377ef1) 1er lot Gemini : sensibilité au contexte et persistance de navigation pour le SFTP, dropdown + workspace "Tous" pour les Workspaces, alternative "cases à cocher + déplacer la sélection ici" pour contourner la complexité du multi-drag Angular CDK. Ajout des chantiers Tunnels SSH et "Group Exec" (ouverture groupée d'un dossier). Importateur MobaXterm explicitement écarté par l'utilisateur (outils de conversion déjà disponibles).

Publication npm v0.2.0 (2026-07-26)

DateHashRésumé
2026-07-26b20d6a7Champ author du package.json fixé à TooMuhtsh (voir piège #2).
2026-07-266057a38Dépendances runtime (dompurify, jeux d'icônes Iconify) déplacées vers devDependencies — elles sont bundlées par webpack dans dist/index.js, pas nécessaires comme dépendances du paquet publié.
2026-07-26bb758c8Préparation publication npm : placeholder restant corrigé, métadonnées package.json mises à jour.
2026-07-26n/a (npm publish, pas un commit)Publié sur npm en tabby-better-sidebar@0.2.0 via compte npm toomuhtsh (token à accès granulaire avec bypass 2FA, la 2FA standard n'étant pas activée). Indexation de recherche npm plus lente que d'habitude après publication (voir piège #20, creusé plus tard dans la session). Installation du vrai paquet publié (pas la jonction de dev) vérifiée manuellement dans %APPDATA%\tabby\plugins\ — fonctionne correctement. Désactiver le plugin depuis l'UI Extensions supprime la jonction NTFS de dev (piège #1) — à recréer si retour en mode dev.

Fonctionnalités socle (2026-07-26)

Environnement de dev, scaffold initial, puis l'ensemble des fonctionnalités "vision" demandées : favoris, statut live, drag & drop, menus contextuels, icônes personnalisées. Détail technique complet (comportement exact, choix d'implémentation) dans les pièges correspondants d'AI-CONTEXT.html.

DateHashRésumé
2026-07-26e304ff5Picker d'icônes (FontAwesome + Iconify MDI/Tabler + import SVG sanitisé DOMPurify), création de dossier/profil par clic droit (vraie modale native EditProfileModalComponent, piège #17), suppression de profil par clic droit (popup HTML, pas de dialogue Windows natif), fix de la zone de dépôt à 0px de hauteur (piège #19).
2026-07-26bfc8b90Favoris épinglés, statut de connexion live (pastille verte/rouge/grise), glisser-déposer complet (réordonnancement + re-parentage de groupes via recréation/migration/suppression, piège #12), suppression de groupe par clic droit (garde-fou non-vide), édition de profil par clic droit (redirection + pilotage DOM vers Paramètres → Profils, pièges #13/#17), glissoir de redimensionnement pleine hauteur (fix :host, piège #14), masquage du groupe "Sans groupe" quand il est vide.
2026-07-26(commit initial, avant renommage)Environnement de dev (Node.js portable), scaffold du plugin (package.json/tsconfig/webpack/LICENSE), composant de base repris du profile-tree natif, montage manuel dans le DOM (mount.service.ts, le composant natif n'étant pas exporté — voir piège #6), build validé, renommage tabby-sidebar-plustabby-better-sidebar, dépôt créé et poussé sur GitHub (identité TooMuhtsh + email noreply, voir pièges #9/#10).