feat(web): IDE unique comme seule coquille, ancien dashboard retiré
L'IDE (/ide) devient l'unique interface sur tous les supports (navigateur, PWA, app Electron chargent la même SPA). Tout le cycle de vie de l'ancien monde est réintégré dans l'IDE ; AppShell et les vues Dashboard / Sessions / Groups / GroupView / SessionView sont supprimés. - Infra: pile de modals Teleport (stores/modals.ts + ModalHost) et menu contextuel (useContextMenu + ContextMenu), montés dans IdeShell sans le démonter (attaches WS du dock préservées). - Explorer: créer worktree (branche + baseRef + session), travailler sur main, prune, promote (escalade force), supprimer worktree, commit/push, masquer/ supprimer repo, ajouter dépôt local ou cloner. - Sessions: create/kill/resume/fork, hide/unhide, archive/unarchive, masquer les découvertes, supervision AttentionList + réponse aux dialogues. - Groupes: create/delete, composition, lancement de session de groupe (ouvre le terminal dans le dock). - Compte: Réglages et Aide en overlays par-dessus l'IDE ; langue, push/PWA, tokens, connexions git, RGPD, déconnexion. Command Palette et deep-links recâblés pour rester dans l'IDE. - Routes: les anciens chemins rendent IdeShell via meta (overlay/panel/ terminal) ; /workspace/:repoId/:wt inchangé (contrat extension VS Code). Mobile: nav à 6 panneaux (explorer/editor/terminal/git/sessions/groups). La grille multi-terminaux de groupe est retirée (supplantée par la session de groupe unique P6 via --add-dir + le dock multi-onglets).
This commit is contained in:
@@ -2,31 +2,137 @@
|
||||
<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>
|
||||
<button type="button" class="ml-auto rounded p-0.5 hover:bg-surface-2 hover:text-fg" :title="t('ide.menu.newGroup')" @click="openNewGroup">
|
||||
<Plus :size="14" />
|
||||
</button>
|
||||
</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 v-for="g in groups.groups" :key="g.id">
|
||||
<div
|
||||
class="group flex items-center rounded text-xs text-fg-muted hover:bg-surface-2/60"
|
||||
@contextmenu.prevent="openGroupMenu($event, g)"
|
||||
>
|
||||
<button type="button" class="flex min-w-0 flex-1 items-center gap-1.5 px-2 py-1 text-left" @click="toggleExpand(g.id)">
|
||||
<component :is="isExpanded(g.id) ? ChevronDown : ChevronRight" :size="14" class="shrink-0 text-fg-subtle" />
|
||||
<Boxes :size="13" class="shrink-0 text-fg-subtle" />
|
||||
<span class="truncate">{{ g.label }}</span>
|
||||
<span class="ml-auto shrink-0 pl-1 text-[10px] text-fg-subtle">{{ t('groups.repoCount', groups.reposInGroup(g.id).length) }}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 rounded p-0.5 opacity-0 hover:bg-surface-3 hover:text-fg group-hover:opacity-100"
|
||||
:title="t('common.settings')"
|
||||
@click.stop="openGroupMenu($event, g)"
|
||||
>
|
||||
<MoreVertical :size="13" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="isExpanded(g.id)" class="pl-6">
|
||||
<button
|
||||
v-for="s in liveSessionsFor(g.id)"
|
||||
:key="s.id"
|
||||
type="button"
|
||||
class="flex w-full items-center gap-1.5 rounded px-2 py-0.5 text-left text-[11px]"
|
||||
:class="ide.activeDockSessionId === s.id ? 'bg-surface-2 text-fg' : 'text-fg-muted hover:bg-surface-2/60'"
|
||||
@click="ide.openTerminal(s.id)"
|
||||
>
|
||||
<SessionStateBadge :session="s" />
|
||||
<span class="truncate font-mono">{{ sessionLabel(s, worktrees) }}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="mt-0.5 flex w-full items-center gap-1.5 rounded px-2 py-0.5 text-left text-[11px] text-accent hover:bg-surface-2/60 disabled:opacity-40"
|
||||
:disabled="groups.reposInGroup(g.id).length === 0"
|
||||
@click="launch(g)"
|
||||
>
|
||||
<Combine :size="12" /> {{ t('groups.newFeature') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// Panneau Groupes de l'IDE : création, composition, lancement d'une session de groupe et ouverture
|
||||
// des sessions du groupe dans le dock. Tout via le store groups / modals ; aucune navigation.
|
||||
import { ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { Boxes, ExternalLink } from '@lucide/vue';
|
||||
import { Boxes, ChevronDown, ChevronRight, Combine, MoreVertical, Pencil, Plus, Trash2 } from '@lucide/vue';
|
||||
import type { GroupSummary } from '@arboretum/shared';
|
||||
import { useGroupsStore } from '../../stores/groups';
|
||||
import { useWorktreesStore } from '../../stores/worktrees';
|
||||
import { useSessionsStore } from '../../stores/sessions';
|
||||
import { useIdeStore } from '../../stores/ide';
|
||||
import { useModalsStore } from '../../stores/modals';
|
||||
import { useToastsStore } from '../../stores/toasts';
|
||||
import { useContextMenu, type ContextMenuItem } from '../../composables/useContextMenu';
|
||||
import { sessionLabel } from '../../lib/session-label';
|
||||
import SessionStateBadge from '../SessionStateBadge.vue';
|
||||
import GroupSessionModal from '../GroupSessionModal.vue';
|
||||
import GroupCreateModal from './modals/GroupCreateModal.vue';
|
||||
import GroupEditModal from './modals/GroupEditModal.vue';
|
||||
import ConfirmModal from './modals/ConfirmModal.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const groups = useGroupsStore();
|
||||
const worktrees = useWorktreesStore();
|
||||
const sessions = useSessionsStore();
|
||||
const ide = useIdeStore();
|
||||
const modals = useModalsStore();
|
||||
const toasts = useToastsStore();
|
||||
const ctx = useContextMenu();
|
||||
|
||||
const expanded = ref<string[]>([]);
|
||||
const isExpanded = (id: string): boolean => expanded.value.includes(id);
|
||||
function toggleExpand(id: string): void {
|
||||
expanded.value = isExpanded(id) ? expanded.value.filter((x) => x !== id) : [...expanded.value, id];
|
||||
}
|
||||
|
||||
// sessions vivantes du groupe, en version live (store sessions).
|
||||
function liveSessionsFor(id: string) {
|
||||
return groups
|
||||
.sessionsInGroup(id)
|
||||
.map((s) => sessions.sessions.find((x) => x.id === s.id) ?? s)
|
||||
.filter((s) => s.live);
|
||||
}
|
||||
|
||||
function openNewGroup(): void {
|
||||
modals.open(GroupCreateModal);
|
||||
}
|
||||
|
||||
function launch(g: GroupSummary): void {
|
||||
modals.open(GroupSessionModal, { groupId: g.id, repos: groups.reposInGroup(g.id) });
|
||||
}
|
||||
|
||||
function openGroupMenu(e: MouseEvent, g: GroupSummary): void {
|
||||
const items: ContextMenuItem[] = [
|
||||
{
|
||||
label: t('ide.menu.launchGroup'),
|
||||
icon: Combine,
|
||||
disabled: groups.reposInGroup(g.id).length === 0,
|
||||
onSelect: () => launch(g),
|
||||
},
|
||||
{ label: t('ide.menu.editGroup'), icon: Pencil, onSelect: () => void modals.open(GroupEditModal, { groupId: g.id }) },
|
||||
{ label: t('ide.menu.deleteGroup'), icon: Trash2, danger: true, onSelect: () => confirmDelete(g) },
|
||||
];
|
||||
ctx.open(e.clientX, e.clientY, items);
|
||||
}
|
||||
|
||||
function confirmDelete(g: GroupSummary): void {
|
||||
modals.open(ConfirmModal, {
|
||||
title: t('ide.menu.deleteGroup'),
|
||||
message: g.label,
|
||||
danger: true,
|
||||
confirmLabel: t('groups.confirmDelete'),
|
||||
onConfirm: async () => {
|
||||
await groups.deleteGroup(g.id);
|
||||
toasts.success(t('toast.groupDeleted'));
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user