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.
Uniquement en ajouter en tête du chantier concerné. Format d'une ligne :
Date | Hash | Résumé.
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.
| Date | Hash | Ré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-14 | fef60c1 | 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). |
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.
| Date | Hash | Ré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-14 | e37afbf | 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. |
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.
| Date | Hash | Résumé |
|---|---|---|
| 2026-08-14 | (commit en cours) | Validé par l'utilisateur, fusion sur master
(4d5c6a3). |
| 2026-08-14 | 53869ec | 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). |
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.
| Date | Hash | Ré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-10 | 6fb5b64 | 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. |
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.
| Date | Hash | Résumé |
|---|---|---|
| 2026-08-08 | f954bbf | 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. |
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.
| Date | Hash | Résumé |
|---|---|---|
| 2026-08-08 | d951ab2 | 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. |
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.
| Date | Hash | Résumé |
|---|---|---|
| 2026-08-08 | 1abcba5 | 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. |
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.
| Date | Hash | Résumé |
|---|---|---|
| 2026-08-08 | 5922cfa | 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-08 | 5077d32 | 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-08 | 83d9d2f | 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-08 | 83d9d2f | 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. |
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é.
| Date | Hash | Résumé |
|---|---|---|
| 2026-08-08 | 8bef7e9 | 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-08 | cddbbc0 | 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. |
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é.
| Date | Hash | Résumé |
|---|---|---|
| 2026-08-08 | 5dd826e | Validé visuellement par l'utilisateur, fusion sur master. |
| 2026-08-08 | 634496b | 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é. |
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é.
| Date | Hash | Résumé |
|---|---|---|
| 2026-08-08 | 6cd6f7d | Validé visuellement par l'utilisateur, fusion sur master. |
| 2026-08-08 | 145d4eb | 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. |
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é.
| Date | Hash | Résumé |
|---|---|---|
| 2026-08-08 | f4ebf8f | 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-08 | ea02170 | 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-08 | 89838b0, 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. |
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é.
| Date | Hash | Résumé |
|---|---|---|
| 2026-08-08 | 3b8b3f4 | 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-08 | 9810173 | 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 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.
| Date | Hash | Résumé |
|---|---|---|
| 2026-08-08 | df923a1 | package.json à 0.4.0, npm publish (build
build:prod via prepublishOnly), release GitHub v0.4.0. |
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.
| Date | Hash | Résumé |
|---|---|---|
| 2026-08-08 | a389e63 | Validé visuellement par l'utilisateur (les deux vues, fenêtre courte,
non-régression menus/popups/glissoir), fusion sur master. |
| 2026-08-08 | befba34 | 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. |
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.
| Date | Hash | Résumé |
|---|---|---|
| 2026-08-08 | 4a9c1b7 | 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-08 | dbdcf21 | 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-08 | ee07b54 | Bloc Transferts converti : template, confirmations d'annulation, badge et infobulle du
registre en pluriels ICU, Session : {label} dans l'infobulle des lignes. |
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.
| Date | Hash | Résumé |
|---|---|---|
| 2026-08-08 | 054a95d | 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. |
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.
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).
| Date | Hash | Résumé |
|---|---|---|
| 2026-08-08 | d284e62 |
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. |
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.
| Date | Hash | Résumé |
|---|---|---|
| 2026-08-07 | ccb586b |
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. |
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.
defaults.ssh.scripts
(piège #77) — un script de login de dossier, que Tabby
fusionne dans chaque profil du dossier, et où un mot de passe sudo se met couramment en dur.
L'export paraissait correct : le mot de passe voisin avait bien disparu. Ce qui l'a révélé
est un contrôle sur la catégorie du décompte — un mot de passe compté comme
« champ sensible » au lieu de « mot de passe », donc retiré par un filet heuristique et non par la
règle qui aurait dû s'en charger. Vérifier qu'un secret est parti ne suffit pas ; il faut vérifier
par quelle règle.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.
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.
| Date | Hash | Résumé |
|---|---|---|
| 2026-08-03 | e2cea77 |
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. |
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.
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.
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.
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.
| Date | Hash | Résumé |
|---|---|---|
| 2026-08-03 | 0d70d05 |
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. |
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 ».
| Date | Hash | Résumé |
|---|---|---|
| 2026-08-03 | 0696799 |
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-03 | 12ad2b7 |
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. |
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.
| Date | Hash | Résumé |
|---|---|---|
| 2026-08-03 | 3ecac9b |
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-03 | e386b94 |
editProfile() ouvre EditProfileModalComponent directement,
comme tabby-settings pour sa propre liste. Suppression de
clickNativeProfileRow(), watchForNativeModalClose(),
modalWatchInterval et wait(). 15 contrôles CDP. |
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.
| Date | Hash | Résumé |
|---|---|---|
| 2026-08-03 | 3884789 |
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é. |
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.
| Date | Hash | Résumé |
|---|---|---|
| 2026-08-03 | fb4b719 |
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-03 | 551d241 |
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. |
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.
| Date | Hash | Résumé |
|---|---|---|
| 2026-08-03 | 50cfd89 |
seuil → roadmap-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. |
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.
| Date | Hash | Résumé |
|---|---|---|
| 2026-08-03 | 9cf21dc |
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. |
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.
| Date | Hash | Résumé |
|---|---|---|
| 2026-08-03 | 7b3c461 |
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-03 | 0192382 |
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'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.
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.
| Date | Hash | Résumé |
|---|---|---|
| 2026-08-03 | a8f5c59 |
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-03 | 3c17f14 |
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-03 | 68e81c7 |
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-03 | a9c56aa |
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-03 | 161074c |
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. |
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).
| Date | Hash | Résumé |
|---|---|---|
| 2026-08-03 | 6de353c |
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). |
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.
| Date | Hash | Résumé |
|---|---|---|
| 2026-08-03 | ceef2d9 |
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. |
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é.
| Date | Hash | Résumé |
|---|---|---|
| 2026-08-03 | 1ded769 |
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é. |
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.
| Date | Hash | Résumé |
|---|---|---|
| 2026-08-03 | 03cab1a |
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. |
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à.
| Date | Hash | Résumé |
|---|---|---|
| 2026-08-03 | 4e25209 |
É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. |
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.
| Date | Hash | Résumé |
|---|---|---|
| 2026-08-02 | 0114245 |
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. |
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.
| Date | Hash | Résumé |
|---|---|---|
| 2026-08-02 | 45a8ecf |
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-02 | 26c152c |
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-02 | 26c152c |
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 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).
| Date | Hash | Résumé |
|---|---|---|
| 2026-08-02 | cfe6010 |
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-02 | cfe6010 |
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-02 | b4159f1 |
É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-02 | 3128e55 |
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-02 | d9d291b |
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-02 | 03ed7fd |
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-02 | 25dbd4c |
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
#26 — AI-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-02 | n/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. |
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'ApplicationRef — window.ng ne sert à rien, le build étant en production
et ngDevMode à false. État final comparé champ à champ à
l'instantané : identique.
defaults et couleur
préservés, les deux profils suivent, le sous-dossier reste favori et replié sous son
nouvel id, aucun id mort laissé derrière.weight n'est réécrit. L'ancien comportement a été rejoué sous sauvegarde pour vérifier
que le test prouve quelque chose : il reclassait bien deux profils réels de
l'utilisateur (Claude et app.exemple.fr). Restauré et vérifié
dans la foulée.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.
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.
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.
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.
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é.
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.
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.
| Date | Hash | Résumé |
|---|---|---|
| 2026-08-02 | a366696 |
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-02 | e73e578 |
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-02 | 8cff07d |
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-02 | ca6dde1 |
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. |
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.
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.
| Date | Hash | Résumé |
|---|---|---|
| 2026-08-02 | 697a624 |
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-02 | 11d3d54 |
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-02 | 2aa610b |
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-02 | 477b7e5 |
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. |
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.
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.
| Date | Hash | Résumé |
|---|---|---|
| 2026-08-02 | 6d0bad2 |
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-01 | 06262b5 |
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-01 | 4a749f1, 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. |
Demande venue en fin de session, livrée sur branche et validée dans la foulée.
| Date | Hash | Résumé |
|---|---|---|
| 2026-08-01 | e4f0a9d |
« 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(). |
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.
| Date | Hash | Résumé |
|---|---|---|
| 2026-08-01 | 9e7a7c8 |
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-01 | aedc8a7 |
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-01 | 1a516b8 |
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-01 | 576a5d6 · 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-01 | 026dd7b |
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. |
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.
| Date | Hash | Résumé |
|---|---|---|
| 2026-08-01 | 02e7d24 |
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-01 | 02e7d24 |
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. |
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.
| Date | Hash | Ré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. |
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é.
| Date | Hash | Résumé |
|---|---|---|
| 2026-08-01 | 8a89787 |
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-01 | 8a89787 |
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-01 | 8a89787 |
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. |
PROFIL.md, validateur et charte
20260731-204511Session 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.
| Date | Hash | Résumé |
|---|---|---|
| 2026-07-31 | 1e39041 |
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-31 | 07362bc |
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-31 | 71d47c1 |
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-31 | 4bc798a |
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-31 | b5bb425 |
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-31 | f496668 |
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-31 | 945339d |
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-31 | d8b56b8 |
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. |
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.
| Date | Hash | Résumé |
|---|---|---|
| 2026-07-30 | 1419073 |
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-30 | 1419073 |
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-30 | 1419073 |
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-30 | e054ae6 |
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-30 | dcabce7 |
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-30 | efcf3f8 |
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 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.
| Date | Hash | Résumé |
|---|---|---|
| 2026-07-29 | df8ef82 |
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-29 | e6d8136 |
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. |
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.
| Date | Hash | Résumé |
|---|---|---|
| 2026-07-29 | c4422f6 |
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. |
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.
| Date | Hash | Résumé |
|---|---|---|
| 2026-07-29 | a0bca09 | 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-29 | bcc92b8 | 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. |
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.
| Date | Hash | Résumé |
|---|---|---|
| 2026-07-29 | 8b3dcfa | 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-29 | a652f0f | 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. |
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.
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.
| Date | Hash | Résumé |
|---|---|---|
| 2026-07-29 | c3ea59c | 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-29 | e8a0024 | 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-29 | 5af7c12 | 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-29 | 5551483 | 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-29 | 2d06daf | 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-29 | 4460ab1 | 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-29 | 85754d8 | 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-29 | 6c5c74d | 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-29 | a22f2eb | 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-29 | c2732e5 | 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-29 | c3ee956 | 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. |
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.
| Date | Hash | Résumé |
|---|---|---|
| 2026-07-29 | 76085d5 | 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. |
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.
| Date | Hash | Résumé |
|---|---|---|
| 2026-07-29 | 42dcb09 | 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-29 | 42dcb09 | 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-29 | 42dcb09 | É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-29 | n/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. |
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.
| Date | Hash | Résumé |
|---|---|---|
| 2026-07-28 | 42dcb09 | 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-28 | 42dcb09 | 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-28 | 42dcb09 | 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-28 | 42dcb09 | 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-28 | 42dcb09 | 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. |
| Date | Hash | Résumé |
|---|---|---|
| 2026-07-28 | 07ba909 | 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. |
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.
| Date | Hash | Résumé |
|---|---|---|
| 2026-07-28 | 3c60f32 | 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-28 | n/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-28 | cd90b8f | 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. |
Signalé par l'utilisateur : le plugin n'apparaissait plus dans la liste des plugins Tabby.
| Date | Hash | Résumé |
|---|---|---|
| 2026-07-26 | n/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. |
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.
| Date | Hash | Résumé |
|---|---|---|
| 2026-07-26 | n/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). |
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.
| Date | Hash | Résumé |
|---|---|---|
| 2026-07-26 | 42dcb09 | 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. |
Trois échanges successifs avec Gemini (Google) pour challenger la roadmap depuis une perspective dev/sysadmin/gestionnaire, plus une clarification directe de l'utilisateur.
| Date | Hash | Résumé |
|---|---|---|
| 2026-07-26 | 5377ef1 | 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). |
| Date | Hash | Résumé |
|---|---|---|
| 2026-07-26 | b20d6a7 | Champ author du package.json fixé à
TooMuhtsh (voir piège #2). |
| 2026-07-26 | 6057a38 | Dé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-26 | bb758c8 | Préparation publication npm : placeholder restant
corrigé, métadonnées package.json mises à jour. |
| 2026-07-26 | n/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. |
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.
| Date | Hash | Résumé |
|---|---|---|
| 2026-07-26 | e304ff5 | Picker 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-26 | bfc8b90 | Favoris é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-plus → tabby-better-sidebar, dépôt créé et poussé sur
GitHub (identité TooMuhtsh + email noreply, voir pièges #9/#10). |