release: git-arboretum 3.4.0 (visibilité temps réel, historisation), desktop 0.2.0 (Windows, logo), vscode 0.4.1, site 0.4.0
CI / Build & test (Node 22) (push) Successful in 11m12s
CI / Build & test (Node 24) (push) Successful in 10m14s
CI / No em/en dashes (push) Successful in 3s
Deploy site (production) / build-and-deploy (push) Successful in 19s
CI / Pack & boot smoke (Node 22) (push) Has been cancelled

Tout est additif : PROTOCOL_VERSION inchangé, aucune rupture d'API.

Temps réel réellement armé
- `pinSession` n'était appelé nulle part : une session vivante épingle désormais le watcher FS de son
  worktree (`WorktreeManager.syncSessionPin` + `resolveWorktreeForCwd`), donc un worktree où un agent
  écrit se rafraîchit même si personne ne le regarde (mesuré ~350 ms).
- Les abonnements `watch` sortent de `GitPanel`, démonté dès qu'on quitte son onglet, ce qui coupait le
  seul abonnement de toute l'app : `composables/useWatchedWorktrees.ts` (monté dans App.vue) suit le
  worktree actif et les dépôts dépliés, borné à 40.
- Une coupure WS ne laisse plus l'UI sur des listes périmées : rechargement complet au retour.
- `worktree_changes` alimente `worktrees.changeVersion`, consommé par l'arbre de fichiers, le diff
  (son `:version` était câblé à 0) et l'éditeur, qui recharge un tampon propre ou lève la bannière de
  conflit avant la sauvegarde au lieu d'attendre le 409.

Corrélation session ↔ worktree par contenance (`@arboretum/shared/path-match.ts`)
- Un terminal lancé dans un sous-répertoire (« Démarrer le projet ») ou une session de groupe reliée
  par `--add-dir` apparaissent enfin sous leur worktree ; le worktree le plus spécifique gagne.
- Règle unique partagée par le daemon, le web et l'extension.

Historisation
- `commitLog` / `commitDiff` purs, `GET /repos/:id/worktrees/log` et `diff?commit=` (hash strictement
  validé, mêmes bornes que les diffs de fichiers).
- `CommitHistory.vue` sous le panneau Git : commits, marquage des non poussés, diff déplié sur place.

Visibilité
- Compteurs git complets sur chaque worktree de l'arbre et du panneau Groupes (ils n'existaient qu'en
  barre de statut, pour le seul worktree actif), avec upstream et dernier commit en infobulle ;
  `locked`, `prunable` et un dépôt invalide sont désormais visibles.
- Le panneau Groupes montre sa composition réelle (dépôts, worktrees, sessions) et teinte l'explorateur.

Polish visuel
- Les toasts d'erreur, persistants, s'empilaient derrière les modals : téléportés au-dessus.
- Sur mobile, ouvrir un terminal ou changer d'activité n'avait aucun effet visible.
- Tailles de panneaux clampées sur la fenêtre, barres d'onglets sans scrollbar parasite, états de
  chargement et d'erreur dans les trois panneaux, accessibilité des 11 modals centralisée dans
  ModalHost, splitters au clavier, numéros de diff collants, `window.confirm` remplacé.

Windows (daemon et packaging)
- `where.exe`, PowerShell comme shell de lancement, askpass `.cmd` (clone/push HTTPS par PAT),
  `taskkill /T`, `%APPDATA%`, `arboretum install` via tâche planifiée.
- Scripts de build exécutables sur un hôte Windows (`npm.cmd`, extraction sans `unzip` ni `bash`).
- Job CI `windows-latest` conditionné par ENABLE_WINDOWS_BUILD ; procédure runner dans docs/CI_RUNNERS.md.

Logo Debian : cause racine
- Une icône unique de 895×895 atterrissait dans `hicolor/895x895`, répertoire absent d'`index.theme`
  donc ignoré par la spécification freedesktop ; et `executableName` dérivait du nom scopé du paquet
  (`@arboretumdesktop`). Jeu d'icônes standard généré + `executableName: arboretum`, plus
  `deb.synopsis` (description courte vide dans apt) et `Section: devel`.
- Runtime Node embarqué élagué : 205 → 118 Mo.
- Auto-update réparé : la release flottante `desktop-latest` que les binaires interrogent n'existait pas.

Doc et vitrine
- README/README.fr : installation par plateforme, mode serveur web (nginx, LAN), dépannage, variables
  d'environnement, flags manquants.
- Doc in-app réécrite (elle renvoyait aux pages Worktrees et Sessions supprimées).
- Section « Accès distant » dans les Réglages ; le 403 BAD_ORIGIN nomme le flag à ajouter.
- Site : prérequis et registre npm privé (le `npx` affiché renvoyait un 404), téléchargements réels par
  plateforme, section « trois façons de l'utiliser », navigation complétée, 16 clés i18n mortes purgées.

Vérifications : 483 tests unitaires, 14 acceptances E2E vertes (dont p14/p15 nouvelles), captures de
rendu sans erreur console (nouveau `verify-ui.mjs`), .deb reconstruit et contrôlé (icônes aux tailles
standard, entrée .desktop valide).
This commit is contained in:
2026-08-04 13:02:11 +02:00
parent a7e04278fd
commit 63f2697745
127 changed files with 4232 additions and 577 deletions
@@ -0,0 +1,74 @@
<script setup lang="ts">
// Les trois façons d'utiliser Arboretum. Le « mode serveur web » (le daemon servi à ses propres
// appareils) n'était décrit nulle part sur le site, alors que c'est l'usage central du produit ; et
// l'extension VS Code n'avait qu'une carte de fonctionnalité, sans lien ni mode d'emploi.
import { useI18n } from 'vue-i18n';
import { DOWNLOADS, DESKTOP_SRC } from '../lib/links';
const { t } = useI18n();
const ways = [
{
key: 'wayDesktop',
body: 'wayDesktopBody',
code: null,
link: { href: DOWNLOADS.deb, label: 'wayDesktopLink' },
},
{
key: 'wayServer',
body: 'wayServerBody',
code: 'tailscale serve --bg 7317',
link: null,
},
{
key: 'wayVscode',
body: 'wayVscodeBody',
code: null,
link: { href: DOWNLOADS.vsix, label: 'wayVscodeLink' },
},
] as const;
</script>
<template>
<section id="access" class="mx-auto max-w-[1200px] scroll-mt-[84px] px-6 pb-[100px]">
<div v-reveal class="mb-[40px] max-w-[720px]">
<div class="mb-3 font-mono text-xs uppercase tracking-[0.12em] text-accent">{{ t('wayKicker') }}</div>
<h2 class="m-0 mb-4 text-[clamp(26px,3vw,38px)] font-semibold leading-[1.15] tracking-[-0.025em] text-fg">
{{ t('wayTitle') }}
</h2>
<p class="m-0 text-[16.5px] leading-[1.6] text-fg-muted">{{ t('wayBody') }}</p>
</div>
<div v-reveal class="grid gap-4 md:grid-cols-3">
<div
v-for="w in ways"
:key="w.key"
class="flex flex-col gap-3 rounded-[14px] border border-border bg-surface-0 p-5 shadow-card"
>
<h3 class="m-0 text-[15px] font-semibold text-fg">{{ t(w.key) }}</h3>
<p class="m-0 flex-1 text-[14px] leading-[1.55] text-fg-muted">{{ t(w.body) }}</p>
<code
v-if="w.code"
class="block overflow-x-auto whitespace-nowrap rounded-lg border border-border bg-surface-1 px-3 py-2 font-mono text-[12.5px] text-fg"
>
<span class="text-accent">$ </span>{{ w.code }}
</code>
<a
v-if="w.link"
:href="w.link.href"
class="text-[13.5px] font-medium text-accent no-underline hover:underline"
>
{{ t(w.link.label) }} →
</a>
</div>
</div>
<p v-reveal class="mt-6 max-w-[760px] text-[13.5px] leading-[1.6] text-fg-subtle">{{ t('wayOriginNote') }}</p>
<p v-reveal class="mt-2 max-w-[760px] text-[13.5px] leading-[1.6] text-fg-subtle">
{{ t('wayBuildNote') }}
<a :href="DESKTOP_SRC" target="_blank" rel="noopener" class="text-fg-muted underline decoration-border underline-offset-2 hover:text-accent">
packages/desktop
</a>
</p>
</section>
</template>
+5 -1
View File
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { INSTALL_COMMAND } from '../composables/useCopy';
import { REPO, LICENSE, COFFEE } from '../lib/links';
import { COFFEE, LICENSE, REPO, VERSIONS } from '../lib/links';
import IconGitea from './icons/IconGitea.vue';
import CopyButton from './CopyButton.vue';
@@ -44,6 +44,10 @@ const { t } = useI18n();
<code class="font-mono text-[12.5px] text-fg-subtle">{{ INSTALL_COMMAND }}</code>
<CopyButton variant="icon" :text="INSTALL_COMMAND" :label="t('copyCommand')" />
</div>
<!-- Versions publiées : le site n'en affichait aucune, impossible de savoir ce qu'il décrit. -->
<span class="font-mono text-[12px] text-fg-subtle">
daemon {{ VERSIONS.daemon }} · desktop {{ VERSIONS.desktop }} · vscode {{ VERSIONS.vscode }}
</span>
</div>
</div>
</footer>
@@ -7,9 +7,13 @@ import IconGitea from './icons/IconGitea.vue';
const { t } = useI18n();
// Les sections #launch (« Démarrer le projet ») et #remotegit (services git distants) existaient sans
// aucun lien de navigation : elles n'étaient atteignables qu'en scrollant à l'aveugle.
const navLinks = [
{ href: '#features', key: 'navFeatures' },
{ href: '#workspace', key: 'navWorkspace' },
{ href: '#launch', key: 'navLaunch' },
{ href: '#remotegit', key: 'navRemoteGit' },
{ href: '#download', key: 'navDownload' },
{ href: '#how', key: 'navHow' },
{ href: '#security', key: 'navSecurity' },
@@ -1,13 +1,17 @@
<script setup lang="ts">
// Téléchargements réels : chaque plateforme porte son lien direct vers l'asset de la release flottante
// `desktop-latest` (le tag est recréé par la CI à chaque version, les URL restent donc valables).
// Avant, les trois cartes étaient décoratives et un unique bouton renvoyait vers la page des releases.
import { useI18n } from 'vue-i18n';
import { RELEASES, DESKTOP_SRC } from '../lib/links';
import { DOWNLOADS, DESKTOP_SRC, RELEASES, VERSIONS } from '../lib/links';
const { t } = useI18n();
const platforms = [
{ key: 'dlLinux', hint: 'dlLinuxHint' },
{ key: 'dlWin', hint: 'dlWinHint' },
{ key: 'dlMac', hint: 'dlMacHint' },
{ key: 'dlLinux', hint: 'dlLinuxHint', href: DOWNLOADS.deb },
{ key: 'dlWin', hint: 'dlWinHint', href: DOWNLOADS.windows },
// macOS n'est pas buildé par la CI (aucun runner) : la carte renvoie donc vers les sources.
{ key: 'dlMac', hint: 'dlMacHint', href: DESKTOP_SRC },
] as const;
const bullets = ['dlNoNode', 'dlBundled', 'dlAutoUpdate'] as const;
@@ -21,22 +25,39 @@ const bullets = ['dlNoNode', 'dlBundled', 'dlAutoUpdate'] as const;
{{ t('dlTitle') }}
</h2>
<p class="m-0 text-[16.5px] leading-[1.6] text-fg-muted">{{ t('dlBody') }}</p>
<p class="mt-2 font-mono text-[12.5px] text-fg-subtle">{{ t('dlVersion', { version: VERSIONS.desktop }) }}</p>
</div>
<div v-reveal class="grid gap-4 md:grid-cols-3">
<div
<a
v-for="p in platforms"
:key="p.key"
class="flex flex-col gap-1 rounded-[14px] border border-border bg-surface-0 p-5 shadow-card"
:href="p.href"
class="group flex flex-col gap-1 rounded-[14px] border border-border bg-surface-0 p-5 no-underline shadow-card transition-colors hover:border-accent/50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent/70"
>
<div class="flex items-center gap-2 text-fg">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-accent" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /><polyline points="7 10 12 15 17 10" /><line x1="12" x2="12" y1="15" y2="3" /></svg>
<span class="text-[15px] font-semibold">{{ t(p.key) }}</span>
<span class="text-[15px] font-semibold group-hover:text-accent">{{ t(p.key) }}</span>
</div>
<span class="pl-[26px] font-mono text-[12.5px] text-fg-subtle">{{ t(p.hint) }}</span>
</div>
</a>
</div>
<p v-reveal class="mt-3 flex flex-wrap items-center gap-x-4 gap-y-1 text-[13px] text-fg-subtle">
<a
:href="DOWNLOADS.appImage"
class="text-fg-muted underline decoration-border underline-offset-2 hover:text-accent"
>
{{ t('dlLinuxAlt') }}
</a>
<a :href="RELEASES" target="_blank" rel="noopener" class="text-fg-muted underline decoration-border underline-offset-2 hover:text-accent">
{{ t('dlAllAssets') }}
</a>
<a :href="DESKTOP_SRC" target="_blank" rel="noopener" class="text-fg-muted underline decoration-border underline-offset-2 hover:text-accent">
{{ t('dlSource') }}
</a>
</p>
<ul v-reveal class="mt-6 flex flex-wrap gap-x-6 gap-y-2">
<li v-for="b in bullets" :key="b" class="flex items-center gap-2 text-[14px] text-fg-muted">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" class="text-accent" aria-hidden="true"><path d="M20 6 9 17l-5-5" /></svg>
@@ -44,25 +65,9 @@ const bullets = ['dlNoNode', 'dlBundled', 'dlAutoUpdate'] as const;
</li>
</ul>
<div v-reveal class="mt-7 flex flex-wrap items-center gap-3">
<a
:href="RELEASES"
target="_blank"
rel="noopener"
class="inline-flex items-center gap-2 rounded-lg bg-accent-solid px-4 py-2.5 text-[14.5px] font-semibold text-white no-underline transition-colors hover:bg-accent-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent/70"
>
{{ t('dlGet') }}
</a>
<a
:href="DESKTOP_SRC"
target="_blank"
rel="noopener"
class="inline-flex items-center gap-2 rounded-lg border border-border px-4 py-2.5 text-[14.5px] font-medium text-fg-muted no-underline transition-colors hover:border-accent/50 hover:text-accent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent/70"
>
{{ t('dlSource') }}
</a>
</div>
<p v-reveal class="mt-4 max-w-[720px] text-[13.5px] leading-[1.55] text-fg-subtle">{{ t('dlNote') }}</p>
<!-- Premier lancement : ni l'installeur Windows ni l'app macOS ne sont signés. Le dire ICI évite
qu'un visiteur conclue à un binaire cassé. -->
<p v-reveal class="mt-6 max-w-[720px] text-[13.5px] leading-[1.55] text-fg-subtle">{{ t('dlUnsigned') }}</p>
<p v-reveal class="mt-2 max-w-[720px] text-[13.5px] leading-[1.55] text-fg-subtle">{{ t('dlNote') }}</p>
</section>
</template>
@@ -127,6 +127,20 @@ const { t } = useI18n();
</template>
{{ t('feat16Desc') }}
</FeatureCard>
<FeatureCard :title="t('feat17Title')">
<template #icon>
<svg width="21" height="21" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 3v5h5" /><path d="M3.05 13A9 9 0 1 0 6 5.3L3 8" /><path d="M12 7v5l4 2" /></svg>
</template>
{{ t('feat17Desc') }}
</FeatureCard>
<FeatureCard :title="t('feat18Title')">
<template #icon>
<svg width="21" height="21" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 20a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z" /><path d="M12 14a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z" /><path d="M12 2v2" /><path d="M12 20v2" /><path d="m4.93 4.93 1.41 1.41" /><path d="m17.66 17.66 1.41 1.41" /><path d="M2 12h2" /><path d="M20 12h2" /></svg>
</template>
{{ t('feat18Desc') }}
</FeatureCard>
</div>
</section>
</template>
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { INSTALL_COMMAND } from '../composables/useCopy';
import { NPMRC_LINE } from '../lib/links';
import CopyButton from './CopyButton.vue';
const { t } = useI18n();
@@ -15,6 +16,20 @@ const LOCAL_URL = 'http://localhost:7317';
<h2 class="m-0 text-[clamp(28px,3.4vw,40px)] font-semibold tracking-[-0.025em] text-fg">{{ t('howTitle') }}</h2>
</div>
<!-- Prérequis : le paquet vit sur un registre npm PRIVÉ. Sans cette ligne dans ~/.npmrc, le `npx`
de l'étape 1 renvoie un 404. C'était le premier mur pour tout nouvel arrivant. -->
<div v-reveal class="mb-4 rounded-xl border border-border bg-surface-1/50 p-[26px]">
<h3 class="m-0 mb-2.5 text-lg font-semibold text-fg">{{ t('prereqTitle') }}</h3>
<p class="m-0 mb-4 text-[14.5px] leading-[1.55] text-fg-muted">{{ t('prereqDesc') }}</p>
<div class="flex items-center gap-2 rounded-lg border border-border bg-surface-0 p-[11px]">
<code class="min-w-0 flex-1 overflow-x-auto whitespace-nowrap font-mono text-[12.5px] text-fg">
{{ NPMRC_LINE }}
</code>
<CopyButton variant="icon" :text="NPMRC_LINE" :label="t('copyCommand')" />
</div>
<p class="m-0 mt-3 text-[13.5px] leading-[1.55] text-fg-subtle">{{ t('prereqNote') }}</p>
</div>
<div v-reveal class="grid grid-cols-[repeat(auto-fit,minmax(280px,1fr))] gap-4">
<div class="rounded-xl border border-border bg-surface-1/50 p-[26px]">
<div class="mb-[18px] font-mono text-[13px] text-accent">01</div>
@@ -61,6 +61,10 @@ function marker(type: DiffLine['type']): string {
<span class="flex h-9 w-9 items-center justify-center rounded-lg" :title="t('wsTerminal')">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m7 11 2-2-2-2" /><path d="M11 13h4" /><rect width="18" height="18" x="3" y="3" rx="2" /></svg>
</span>
<!-- 4e onglet : Groupes (l'app en a quatre, le mockup n'en montrait que trois) -->
<span class="flex h-9 w-9 items-center justify-center rounded-lg" :title="t('wsGroups')">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m7.5 4.27 9 5.15" /><path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z" /><path d="m3.3 7 8.7 5 8.7-5" /><path d="M12 22V12" /></svg>
</span>
</nav>
<!-- arbre unifie : plusieurs projets a la fois -->
@@ -77,6 +81,12 @@ function marker(type: DiffLine['type']): string {
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0 text-fg-subtle" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="var(--color-accent)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0" aria-hidden="true"><line x1="6" x2="6" y1="3" y2="15" /><circle cx="18" cy="6" r="3" /><circle cx="6" cy="18" r="3" /><path d="M18 9a9 9 0 0 1-9 9" /></svg>
<span class="truncate text-accent">feat/auth</span>
<!-- compteurs git compacts : ahead / indexés / non indexés, comme dans l'app -->
<span class="ml-auto flex shrink-0 items-center gap-1 text-[11px]">
<span class="text-accent">↑2</span>
<span class="text-accent">●1</span>
<span class="text-warn">○1</span>
</span>
</div>
<!-- session claude corrélée -->
<div class="flex items-center gap-1.5 rounded py-0.5 pr-2 pl-[38px] text-fg-muted">
@@ -94,6 +104,7 @@ function marker(type: DiffLine['type']): string {
<div class="flex items-center gap-1 rounded py-0.5 pr-2 pl-1.5 text-fg-muted">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0 text-fg-subtle" aria-hidden="true"><path d="m9 18 6-6-6-6" /></svg>
<span class="font-medium">web</span>
<span class="ml-auto shrink-0 text-[11px] text-warn">○3</span>
</div>
<div class="flex items-center gap-1 rounded py-0.5 pr-2 pl-1.5 text-fg-muted">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0 text-fg-subtle" aria-hidden="true"><path d="m9 18 6-6-6-6" /></svg>