feat(web): panneaux IDE Git / Sessions / Groupes
components/ide/GitPanel.vue : CommitPanel ciblant le worktree actif (ide.activeContext), chargement des changements + abonnement disque (watchWorktree) ré-abonné au changement de worktree ; sélection d'un fichier ouvre un onglet diff. SessionsPanel : sessions vivantes -> ouverture dans le dock. GroupsPanel : groupes (getters) avec passerelle vers la vue Groupes. PrimarySidebar aiguille désormais Explorer/Git/Sessions/Groupes vers ces panneaux (fin des placeholders). vue-tsc + build web verts.
This commit is contained in:
94
packages/web/src/components/ide/GitPanel.vue
Normal file
94
packages/web/src/components/ide/GitPanel.vue
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex h-full min-h-0 flex-col">
|
||||||
|
<template v-if="activeWt">
|
||||||
|
<CommitPanel
|
||||||
|
:repo-id="activeWt.repoId"
|
||||||
|
:wt="activeWt.path"
|
||||||
|
:changes="changes"
|
||||||
|
:git="activeWt.git"
|
||||||
|
:truncated="truncated"
|
||||||
|
:active="activeFile"
|
||||||
|
@select="onSelect"
|
||||||
|
@changed="load"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<div v-else class="flex h-full items-center justify-center p-4">
|
||||||
|
<EmptyState :icon="GitCompare" :title="t('ide.activity.git')" :hint="t('workspace.noFileHint')" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// Panneau Git de l'IDE : cible le worktree actif (ide.activeContext), charge ses changements et
|
||||||
|
// s'abonne aux modifications disque. Réutilise CommitPanel (staging, commit, fetch/pull) tel quel.
|
||||||
|
import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { GitCompare } from '@lucide/vue';
|
||||||
|
import type { FileChange } from '@arboretum/shared';
|
||||||
|
import { useIdeStore, tabId } from '../../stores/ide';
|
||||||
|
import { useWorktreesStore } from '../../stores/worktrees';
|
||||||
|
import { gitApi } from '../../lib/git-api';
|
||||||
|
import { wsClient } from '../../lib/ws-client';
|
||||||
|
import CommitPanel from '../workspace/CommitPanel.vue';
|
||||||
|
import EmptyState from '../ui/EmptyState.vue';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const ide = useIdeStore();
|
||||||
|
const worktrees = useWorktreesStore();
|
||||||
|
|
||||||
|
const changes = ref<FileChange[]>([]);
|
||||||
|
const truncated = ref(false);
|
||||||
|
|
||||||
|
const activeWt = computed(() => {
|
||||||
|
const c = ide.activeContext;
|
||||||
|
if (!c) return null;
|
||||||
|
return worktrees.worktrees.find((w) => w.repoId === c.repoId && w.path === c.wtPath) ?? null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeFile = computed(() => {
|
||||||
|
const tab = ide.activeTab;
|
||||||
|
const wt = activeWt.value;
|
||||||
|
return tab && wt && tab.repoId === wt.repoId && tab.wtPath === wt.path ? tab.file : null;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function load(): Promise<void> {
|
||||||
|
const wt = activeWt.value;
|
||||||
|
if (!wt) {
|
||||||
|
changes.value = [];
|
||||||
|
truncated.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await gitApi.changes(wt.repoId, wt.path);
|
||||||
|
changes.value = res.changes;
|
||||||
|
truncated.value = res.truncated;
|
||||||
|
} catch {
|
||||||
|
/* worktree transitoire / erreur réseau : on garde l'état précédent */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSelect(sel: { file: string; staged: boolean }): void {
|
||||||
|
const wt = activeWt.value;
|
||||||
|
if (!wt) return;
|
||||||
|
ide.openFile(wt.repoId, wt.path, sel.file, 'diff');
|
||||||
|
ide.setTabView(tabId(wt.repoId, wt.path, sel.file), 'diff', sel.staged);
|
||||||
|
}
|
||||||
|
|
||||||
|
// (ré)abonnement disque ciblé sur le worktree actif ; rechargement des changements.
|
||||||
|
let unwatch: (() => void) | null = null;
|
||||||
|
watch(
|
||||||
|
() => activeWt.value?.path,
|
||||||
|
() => {
|
||||||
|
unwatch?.();
|
||||||
|
unwatch = null;
|
||||||
|
void load();
|
||||||
|
const wt = activeWt.value;
|
||||||
|
if (wt) {
|
||||||
|
unwatch = wsClient.watchWorktree(wt.repoId, wt.path, () => void load());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
onBeforeUnmount(() => unwatch?.());
|
||||||
|
</script>
|
||||||
32
packages/web/src/components/ide/GroupsPanel.vue
Normal file
32
packages/web/src/components/ide/GroupsPanel.vue
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex h-full min-h-0 flex-col">
|
||||||
|
<div class="flex items-center gap-1 px-3 py-2 text-[11px] font-semibold tracking-wide text-fg-subtle uppercase">
|
||||||
|
{{ t('ide.activity.groups') }}
|
||||||
|
<RouterLink :to="{ name: 'groups' }" class="ml-auto rounded p-0.5 hover:bg-surface-2 hover:text-fg" :title="t('nav.groups')">
|
||||||
|
<ExternalLink :size="12" />
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
<div class="min-h-0 flex-1 overflow-auto px-1 pb-2">
|
||||||
|
<p v-if="groups.groups.length === 0" class="px-2 py-1 text-xs text-fg-subtle">{{ t('groups.empty') }}</p>
|
||||||
|
<RouterLink
|
||||||
|
v-for="g in groups.groups"
|
||||||
|
:key="g.id"
|
||||||
|
:to="{ name: 'group', params: { id: g.id } }"
|
||||||
|
class="flex items-center gap-2 rounded px-2 py-1 text-xs text-fg-muted hover:bg-surface-2/60"
|
||||||
|
>
|
||||||
|
<Boxes :size="13" class="shrink-0 text-fg-subtle" />
|
||||||
|
<span class="truncate">{{ g.label }}</span>
|
||||||
|
<span class="ml-auto shrink-0 text-[10px] text-fg-subtle">{{ t('groups.repoCount', groups.reposInGroup(g.id).length) }}</span>
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { Boxes, ExternalLink } from '@lucide/vue';
|
||||||
|
import { useGroupsStore } from '../../stores/groups';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const groups = useGroupsStore();
|
||||||
|
</script>
|
||||||
@@ -4,32 +4,18 @@
|
|||||||
:style="{ width: `${ide.leftWidth}px` }"
|
:style="{ width: `${ide.leftWidth}px` }"
|
||||||
>
|
>
|
||||||
<ProjectTree v-if="ide.activeActivity === 'explorer'" />
|
<ProjectTree v-if="ide.activeActivity === 'explorer'" />
|
||||||
<!-- Git / Sessions / Groupes : panneaux dédiés livrés en B6 ; placeholder pour l'instant. -->
|
<GitPanel v-else-if="ide.activeActivity === 'git'" />
|
||||||
<div v-else class="flex h-full items-center justify-center p-4">
|
<SessionsPanel v-else-if="ide.activeActivity === 'sessions'" />
|
||||||
<EmptyState :icon="placeholder.icon" :title="placeholder.title" :hint="t('ide.comingSoon')" />
|
<GroupsPanel v-else />
|
||||||
</div>
|
|
||||||
</aside>
|
</aside>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
|
||||||
import { useI18n } from 'vue-i18n';
|
|
||||||
import { Boxes, GitCompare, SquareTerminal } from '@lucide/vue';
|
|
||||||
import { useIdeStore } from '../../stores/ide';
|
import { useIdeStore } from '../../stores/ide';
|
||||||
import ProjectTree from './ProjectTree.vue';
|
import ProjectTree from './ProjectTree.vue';
|
||||||
import EmptyState from '../ui/EmptyState.vue';
|
import GitPanel from './GitPanel.vue';
|
||||||
|
import SessionsPanel from './SessionsPanel.vue';
|
||||||
|
import GroupsPanel from './GroupsPanel.vue';
|
||||||
|
|
||||||
const { t } = useI18n();
|
|
||||||
const ide = useIdeStore();
|
const ide = useIdeStore();
|
||||||
|
|
||||||
const placeholder = computed(() => {
|
|
||||||
switch (ide.activeActivity) {
|
|
||||||
case 'git':
|
|
||||||
return { icon: GitCompare, title: t('ide.activity.git') };
|
|
||||||
case 'sessions':
|
|
||||||
return { icon: SquareTerminal, title: t('ide.activity.sessions') };
|
|
||||||
default:
|
|
||||||
return { icon: Boxes, title: t('ide.activity.groups') };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
53
packages/web/src/components/ide/SessionsPanel.vue
Normal file
53
packages/web/src/components/ide/SessionsPanel.vue
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex h-full min-h-0 flex-col">
|
||||||
|
<div class="flex items-center gap-1 px-3 py-2 text-[11px] font-semibold tracking-wide text-fg-subtle uppercase">
|
||||||
|
{{ t('ide.activity.sessions') }}
|
||||||
|
<RouterLink :to="{ name: 'sessions' }" class="ml-auto rounded p-0.5 hover:bg-surface-2 hover:text-fg" :title="t('nav.sessions')">
|
||||||
|
<ExternalLink :size="12" />
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
<div class="min-h-0 flex-1 overflow-auto px-1 pb-2">
|
||||||
|
<p v-if="liveSessions.length === 0" class="px-2 py-1 text-xs text-fg-subtle">{{ t('sessions.empty') }}</p>
|
||||||
|
<button
|
||||||
|
v-for="s in liveSessions"
|
||||||
|
:key="s.id"
|
||||||
|
type="button"
|
||||||
|
class="flex w-full items-center gap-1.5 rounded px-2 py-1 text-left text-xs hover:bg-surface-2/60"
|
||||||
|
:class="ide.activeDockSessionId === s.id ? 'bg-surface-2 text-fg' : 'text-fg-muted'"
|
||||||
|
@click="ide.openTerminal(s.id)"
|
||||||
|
>
|
||||||
|
<SessionStateBadge :session="s" />
|
||||||
|
<span class="truncate font-mono">{{ titleFor(s) }}</span>
|
||||||
|
<span class="ml-auto shrink-0 font-mono text-[10px] text-fg-subtle">{{ s.command }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { ExternalLink } from '@lucide/vue';
|
||||||
|
import type { SessionSummary } from '@arboretum/shared';
|
||||||
|
import { useIdeStore } from '../../stores/ide';
|
||||||
|
import { useSessionsStore } from '../../stores/sessions';
|
||||||
|
import { useWorktreesStore } from '../../stores/worktrees';
|
||||||
|
import SessionStateBadge from '../SessionStateBadge.vue';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const ide = useIdeStore();
|
||||||
|
const sessions = useSessionsStore();
|
||||||
|
const worktrees = useWorktreesStore();
|
||||||
|
|
||||||
|
const liveSessions = computed(() => sessions.sessions.filter((s) => s.live));
|
||||||
|
|
||||||
|
function titleFor(s: SessionSummary): string {
|
||||||
|
const wt = worktrees.worktrees.find((w) => w.path === s.cwd);
|
||||||
|
if (wt) {
|
||||||
|
const repo = worktrees.repos.find((r) => r.id === wt.repoId);
|
||||||
|
const branch = wt.branch ?? wt.head.slice(0, 7);
|
||||||
|
return repo ? `${repo.label} · ${branch}` : branch;
|
||||||
|
}
|
||||||
|
return s.cwd.split('/').filter(Boolean).pop() ?? s.cwd;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
Reference in New Issue
Block a user