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
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:
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<div class="flex min-h-0 flex-col border-t border-border">
|
||||
<!-- deux boutons frères (jamais imbriqués : un bouton dans un bouton est du HTML invalide) -->
|
||||
<div class="flex shrink-0 items-center gap-1 px-2 py-1 label-mono">
|
||||
<button type="button" class="flex min-w-0 flex-1 items-center gap-1 text-left hover:text-fg" :aria-expanded="open" @click="open = !open">
|
||||
<component :is="open ? ChevronDown : ChevronRight" :size="13" class="shrink-0" />
|
||||
<History :size="13" class="shrink-0" />
|
||||
<span class="truncate">{{ t('history.title') }}</span>
|
||||
<span v-if="unpushedTotal > 0" class="shrink-0 rounded bg-warn/15 px-1 text-[10px] text-warn" :title="t('history.unpushedHint')">
|
||||
{{ t('history.unpushed', { n: unpushedTotal }) }}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="open"
|
||||
type="button"
|
||||
class="shrink-0 rounded p-0.5 hover:bg-surface-2 hover:text-fg"
|
||||
:title="t('common.refresh')"
|
||||
@click="load"
|
||||
>
|
||||
<RefreshCw :size="12" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="open" class="min-h-0 flex-1 overflow-auto">
|
||||
<p v-if="loading && commits.length === 0" class="px-2 py-1 text-xs text-fg-subtle">{{ t('fs.loading') }}</p>
|
||||
<p v-else-if="error" class="px-2 py-1 text-xs text-danger">{{ error }}</p>
|
||||
<p v-else-if="commits.length === 0" class="px-2 py-1 text-xs text-fg-subtle">{{ t('history.empty') }}</p>
|
||||
|
||||
<template v-for="(c, i) in commits" :key="c.hash">
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-baseline gap-2 px-2 py-1 text-left text-xs hover:bg-surface-2/60"
|
||||
:class="selected === c.hash ? 'bg-surface-2' : ''"
|
||||
:title="`${c.subject}\n${c.author} · ${absoluteDate(c.date)}\n${c.hash}`"
|
||||
@click="toggle(c.hash)"
|
||||
>
|
||||
<span class="shrink-0 font-mono text-[11px]" :class="isUnpushed(i) ? 'text-warn' : 'text-fg-subtle'">
|
||||
{{ c.shortHash }}
|
||||
</span>
|
||||
<span class="min-w-0 flex-1 truncate">{{ c.subject }}</span>
|
||||
<span class="shrink-0 text-[10px] text-fg-subtle">{{ relativeDate(c.date) }}</span>
|
||||
</button>
|
||||
<DiffViewer
|
||||
v-if="selected === c.hash"
|
||||
:repo-id="repoId"
|
||||
:wt="wt"
|
||||
:commit="c.hash"
|
||||
class="max-h-80 border-y border-border bg-surface-1/40"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<button
|
||||
v-if="canLoadMore"
|
||||
type="button"
|
||||
class="w-full px-2 py-1 text-left text-[11px] text-fg-subtle hover:text-fg"
|
||||
:disabled="loading"
|
||||
@click="loadMore"
|
||||
>
|
||||
{{ t('history.loadMore') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// Historique de la branche du worktree : ce qui a déjà été acté, et ce qui n'est pas encore poussé.
|
||||
// Complète le panneau de changements (« ce qui n'est pas commité ») pour donner l'état complet du
|
||||
// travail en cours sans ouvrir un terminal. Le diff d'un commit se déplie sur place, dans la même vue
|
||||
// que les diffs de fichiers (DiffViewer accepte les deux formes).
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { ChevronDown, ChevronRight, History, RefreshCw } from '@lucide/vue';
|
||||
import type { CommitEntry } from '@arboretum/shared';
|
||||
import { gitApi } from '../../lib/git-api';
|
||||
import { formatDateTime, formatRelative } from '../../lib/format';
|
||||
import { persistedRef } from '../../lib/persisted-ref';
|
||||
import DiffViewer from './DiffViewer.vue';
|
||||
|
||||
const props = defineProps<{ repoId: string; wt: string; version: number }>();
|
||||
const { t, locale } = useI18n();
|
||||
|
||||
const PAGE = 30;
|
||||
const open = persistedRef<boolean>('arb.history.open', false);
|
||||
const commits = ref<CommitEntry[]>([]);
|
||||
const unpushedCount = ref(0);
|
||||
const hasUpstream = ref(false);
|
||||
const loading = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
const selected = ref<string | null>(null);
|
||||
const reachedEnd = ref(false);
|
||||
|
||||
/** Sans upstream, AUCUN commit n'est publié : on marque toute la liste plutôt qu'un préfixe. */
|
||||
const isUnpushed = (index: number): boolean => (hasUpstream.value ? index < unpushedCount.value : true);
|
||||
const unpushedTotal = computed(() => (hasUpstream.value ? unpushedCount.value : commits.value.length));
|
||||
const canLoadMore = computed(() => !reachedEnd.value && commits.value.length > 0);
|
||||
|
||||
const absoluteDate = (iso: string): string => formatDateTime(iso, locale.value);
|
||||
const relativeDate = (iso: string): string => formatRelative(iso, locale.value);
|
||||
|
||||
async function load(): Promise<void> {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const res = await gitApi.log(props.repoId, props.wt, { limit: PAGE });
|
||||
commits.value = res.commits;
|
||||
unpushedCount.value = res.unpushedCount;
|
||||
hasUpstream.value = res.hasUpstream;
|
||||
reachedEnd.value = res.commits.length < PAGE;
|
||||
if (selected.value && !res.commits.some((c) => c.hash === selected.value)) selected.value = null;
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : String(err);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMore(): Promise<void> {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await gitApi.log(props.repoId, props.wt, { limit: PAGE, skip: commits.value.length });
|
||||
commits.value = [...commits.value, ...res.commits];
|
||||
reachedEnd.value = res.commits.length < PAGE;
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : String(err);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function toggle(hash: string): void {
|
||||
selected.value = selected.value === hash ? null : hash;
|
||||
}
|
||||
|
||||
// Rechargement au changement de worktree et à chaque modification disque (un commit fait bouger
|
||||
// .git/HEAD, donc le jeton change) ; rien n'est chargé tant que la section est repliée.
|
||||
watch(
|
||||
() => [props.repoId, props.wt, props.version, open.value] as const,
|
||||
() => {
|
||||
if (open.value) void load();
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
@@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<div class="flex h-full flex-col border-t border-border">
|
||||
<!-- pas de bordure haute : ce panneau est le premier enfant du panneau Git (le trait était un
|
||||
vestige de l'ancien layout, où il vivait sous le contenu). -->
|
||||
<div class="flex h-full flex-col">
|
||||
<!-- en-tête + actions distantes -->
|
||||
<div class="flex items-center gap-1 px-2 py-1 label-mono">
|
||||
<GitCompare :size="13" /> {{ t('workspace.changes') }}
|
||||
|
||||
@@ -15,13 +15,13 @@
|
||||
<tbody>
|
||||
<template v-for="(h, hi) in parsed.hunks" :key="hi">
|
||||
<tr class="bg-surface-1/60 text-info/80">
|
||||
<td class="select-none px-2 text-right text-fg-subtle" />
|
||||
<td class="select-none px-2 text-right text-fg-subtle" />
|
||||
<td class="diff-gutter left-0" />
|
||||
<td class="diff-gutter left-10" />
|
||||
<td class="whitespace-pre px-2">{{ h.header }}</td>
|
||||
</tr>
|
||||
<tr v-for="(l, li) in h.lines" :key="`${hi}-${li}`" :class="rowClass(l.type)">
|
||||
<td class="w-10 select-none px-2 text-right text-fg-subtle">{{ l.oldLine ?? '' }}</td>
|
||||
<td class="w-10 select-none px-2 text-right text-fg-subtle">{{ l.newLine ?? '' }}</td>
|
||||
<td class="diff-gutter left-0">{{ l.oldLine ?? '' }}</td>
|
||||
<td class="diff-gutter left-10">{{ l.newLine ?? '' }}</td>
|
||||
<td class="whitespace-pre px-2">{{ marker(l.type) }}{{ l.content }}</td>
|
||||
</tr>
|
||||
</template>
|
||||
@@ -37,7 +37,12 @@ import { useI18n } from 'vue-i18n';
|
||||
import { gitApi } from '../../lib/git-api';
|
||||
import { parseUnifiedDiff, type DiffLineType, type ParsedDiff } from '../../lib/diff-parse';
|
||||
|
||||
const props = defineProps<{ repoId: string; wt: string; file: string; staged: boolean; version: number }>();
|
||||
// Deux formes, un seul rendu : diff d'un FICHIER (`file`, éventuellement indexé) ou diff complet d'un
|
||||
// COMMIT (`commit`). Les deux sont des diffs unifiés, donc le même parseur et la même table.
|
||||
const props = withDefaults(
|
||||
defineProps<{ repoId: string; wt: string; file?: string; commit?: string; staged?: boolean; version?: number }>(),
|
||||
{ file: '', commit: '', staged: false, version: 0 },
|
||||
);
|
||||
const { t } = useI18n();
|
||||
|
||||
const loading = ref(false);
|
||||
@@ -47,12 +52,15 @@ const tooLarge = ref(false);
|
||||
const parsed = ref<ParsedDiff>({ hunks: [], additions: 0, deletions: 0 });
|
||||
|
||||
async function load(): Promise<void> {
|
||||
if (!props.file && !props.commit) return;
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
binary.value = false;
|
||||
tooLarge.value = false;
|
||||
try {
|
||||
const res = await gitApi.diff(props.repoId, props.wt, props.file, props.staged);
|
||||
const res = props.commit
|
||||
? await gitApi.commitDiff(props.repoId, props.wt, props.commit)
|
||||
: await gitApi.diff(props.repoId, props.wt, props.file, props.staged);
|
||||
binary.value = res.binary;
|
||||
tooLarge.value = res.tooLarge;
|
||||
parsed.value = res.binary || res.tooLarge ? { hunks: [], additions: 0, deletions: 0 } : parseUnifiedDiff(res.diff);
|
||||
@@ -63,7 +71,13 @@ async function load(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => [props.repoId, props.wt, props.file, props.staged, props.version], load, { immediate: true });
|
||||
// Un commit est immuable : sa version n'entre pas dans les dépendances (inutile de recharger un diff
|
||||
// figé à chaque écriture disque).
|
||||
watch(
|
||||
() => [props.repoId, props.wt, props.file, props.commit, props.staged, props.commit ? 0 : props.version],
|
||||
load,
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function rowClass(type: DiffLineType): string {
|
||||
if (type === 'add') return 'diff-add';
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
:wt="wt"
|
||||
:depth="depth"
|
||||
:active="active"
|
||||
:version="version"
|
||||
@open="(p) => emit('open', p)"
|
||||
/>
|
||||
</div>
|
||||
@@ -31,9 +32,13 @@ import type { FsEntry, FsListResponse } from '@arboretum/shared';
|
||||
import { api } from '../../lib/api';
|
||||
import FileTreeNode from './FileTreeNode.vue';
|
||||
|
||||
const props = withDefaults(defineProps<{ wt: string; active: string | null; embedded?: boolean; depth?: number }>(), {
|
||||
// `version` = jeton d'invalidation du worktree (stores/worktrees.changeVersion) : il change à chaque
|
||||
// `worktree_changes` reçu, ce qui recharge l'arbre sans polling. Sans lui, un fichier créé ou supprimé
|
||||
// par une session n'apparaissait jamais tant qu'on ne repliait pas le nœud.
|
||||
const props = withDefaults(defineProps<{ wt: string; active: string | null; embedded?: boolean; depth?: number; version?: number }>(), {
|
||||
embedded: false,
|
||||
depth: 0,
|
||||
version: 0,
|
||||
});
|
||||
const emit = defineEmits<{ open: [relPath: string] }>();
|
||||
const { t } = useI18n();
|
||||
@@ -56,5 +61,5 @@ async function load(): Promise<void> {
|
||||
}
|
||||
|
||||
onMounted(load);
|
||||
watch(() => props.wt, load);
|
||||
watch([() => props.wt, () => props.version], load);
|
||||
</script>
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
:wt="wt"
|
||||
:depth="depth + 1"
|
||||
:active="active"
|
||||
:version="version"
|
||||
@open="(p) => emit('open', p)"
|
||||
/>
|
||||
</template>
|
||||
@@ -33,12 +34,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { ChevronDown, ChevronRight, File } from '@lucide/vue';
|
||||
import type { FsEntry, FsListResponse } from '@arboretum/shared';
|
||||
import { api } from '../../lib/api';
|
||||
|
||||
const props = defineProps<{ entry: FsEntry; wt: string; depth: number; active: string | null }>();
|
||||
const props = defineProps<{ entry: FsEntry; wt: string; depth: number; active: string | null; version: number }>();
|
||||
const emit = defineEmits<{ open: [relPath: string] }>();
|
||||
|
||||
const expanded = ref(false);
|
||||
@@ -66,6 +67,17 @@ async function loadChildren(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
// Le contenu du worktree a changé sur le disque : on invalide le cache d'enfants. Rechargement
|
||||
// immédiat si le dossier est déplié (donc visible), sinon paresseux au prochain dépliage · seuls les
|
||||
// nœuds réellement affichés paient une requête.
|
||||
watch(
|
||||
() => props.version,
|
||||
() => {
|
||||
loaded = false;
|
||||
if (expanded.value && !props.entry.isFile) void loadChildren();
|
||||
},
|
||||
);
|
||||
|
||||
function onClick(): void {
|
||||
if (props.entry.isFile) {
|
||||
emit('open', relPath.value);
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
<template>
|
||||
<span class="flex items-center gap-2 text-xs">
|
||||
<span class="flex items-center gap-1 font-mono text-fg-muted">
|
||||
<!-- `dense` : variante sans branche ni « propre », pour tenir sur la ligne d'un worktree dans
|
||||
l'arbre de projets. Les compteurs existaient déjà dans le protocole mais n'étaient visibles
|
||||
QUE dans la barre de statut, pour le seul worktree actif. -->
|
||||
<span class="flex items-center text-xs" :class="dense ? 'gap-1' : 'gap-2'" :title="dense ? summary : undefined">
|
||||
<span v-if="!dense" class="flex items-center gap-1 font-mono text-fg-muted">
|
||||
<GitBranch :size="13" />{{ branch ?? t('worktrees.detached') }}
|
||||
</span>
|
||||
<span v-if="git.ahead" class="text-accent" :title="t('git.ahead')">↑{{ git.ahead }}</span>
|
||||
<span v-if="git.behind" class="text-warn" :title="t('git.behind')">↓{{ git.behind }}</span>
|
||||
<span v-if="git.stagedCount" class="text-accent" :title="t('git.staged')">●{{ git.stagedCount }}</span>
|
||||
<span v-if="git.unstagedCount" class="text-warn" :title="t('git.unstaged')">○{{ git.unstagedCount }}</span>
|
||||
<span v-if="git.conflictCount" class="text-danger" :title="t('git.conflicts')">⚠{{ git.conflictCount }}</span>
|
||||
<span v-if="isClean" class="text-fg-subtle">{{ t('worktrees.clean') }}</span>
|
||||
<span v-if="git.ahead" class="text-accent" :title="dense ? undefined : t('git.ahead')">↑{{ git.ahead }}</span>
|
||||
<span v-if="git.behind" class="text-warn" :title="dense ? undefined : t('git.behind')">↓{{ git.behind }}</span>
|
||||
<span v-if="git.stagedCount" class="text-accent" :title="dense ? undefined : t('git.staged')">●{{ git.stagedCount }}</span>
|
||||
<span v-if="git.unstagedCount" class="text-warn" :title="dense ? undefined : t('git.unstaged')">○{{ git.unstagedCount }}</span>
|
||||
<span v-if="git.conflictCount" class="text-danger" :title="dense ? undefined : t('git.conflicts')">⚠{{ git.conflictCount }}</span>
|
||||
<span v-if="!dense && isClean" class="text-fg-subtle">{{ t('worktrees.clean') }}</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
@@ -18,10 +21,32 @@ import { useI18n } from 'vue-i18n';
|
||||
import { GitBranch } from '@lucide/vue';
|
||||
import type { WorktreeGitStatus } from '@arboretum/shared';
|
||||
|
||||
const props = defineProps<{ git: WorktreeGitStatus; branch: string | null }>();
|
||||
const props = withDefaults(defineProps<{ git: WorktreeGitStatus; branch: string | null; dense?: boolean }>(), {
|
||||
dense: false,
|
||||
});
|
||||
const { t } = useI18n();
|
||||
|
||||
const isClean = computed(
|
||||
() => !props.git.dirtyCount && !props.git.ahead && !props.git.behind && !props.git.stagedCount && !props.git.unstagedCount,
|
||||
);
|
||||
|
||||
/**
|
||||
* En mode dense, les symboles seuls sont trop laconiques : on regroupe tout dans une infobulle unique
|
||||
* (compteurs nommés, upstream, dernier commit). C'est là que `upstream`, `lastCommitHash` et
|
||||
* `lastCommitSubject` deviennent enfin visibles quelque part.
|
||||
*/
|
||||
const summary = computed(() => {
|
||||
const g = props.git;
|
||||
const lines: string[] = [props.branch ?? t('worktrees.detached')];
|
||||
const counts: string[] = [];
|
||||
if (g.ahead) counts.push(`↑${g.ahead} ${t('git.ahead')}`);
|
||||
if (g.behind) counts.push(`↓${g.behind} ${t('git.behind')}`);
|
||||
if (g.stagedCount) counts.push(`${g.stagedCount} ${t('git.staged')}`);
|
||||
if (g.unstagedCount) counts.push(`${g.unstagedCount} ${t('git.unstaged')}`);
|
||||
if (g.conflictCount) counts.push(`${g.conflictCount} ${t('git.conflicts')}`);
|
||||
lines.push(counts.length > 0 ? counts.join(' · ') : t('worktrees.clean'));
|
||||
lines.push(g.upstream ? `${t('git.upstream')} ${g.upstream}` : t('git.noUpstream'));
|
||||
if (g.lastCommitHash) lines.push(`${t('git.lastCommit')} ${g.lastCommitHash} ${g.lastCommitSubject ?? ''}`.trim());
|
||||
return lines.join('\n');
|
||||
});
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user