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,52 +2,153 @@
|
||||
<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 type="button" class="ml-auto rounded p-0.5 hover:bg-surface-2 hover:text-fg" :title="t('sessions.newSession')" @click="openAddMenu">
|
||||
<Plus :size="14" />
|
||||
</button>
|
||||
<button type="button" class="rounded p-0.5 hover:bg-surface-2 hover:text-fg" :title="t('common.settings')" @click="openMoreMenu">
|
||||
<MoreHorizontal :size="14" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="min-h-0 flex-1 overflow-auto">
|
||||
<AttentionList />
|
||||
<div class="px-1 pt-1 pb-2">
|
||||
<p v-if="rows.length === 0" class="px-2 py-1 text-xs text-fg-subtle">{{ t('sessions.empty') }}</p>
|
||||
<div
|
||||
v-for="s in rows"
|
||||
:key="s.id"
|
||||
class="group flex w-full items-center gap-1.5 rounded px-2 py-1 text-xs"
|
||||
:class="ide.activeDockSessionId === s.id ? 'bg-surface-2 text-fg' : 'text-fg-muted hover:bg-surface-2/60'"
|
||||
@contextmenu.prevent="openRowMenu($event, s)"
|
||||
>
|
||||
<button type="button" class="flex min-w-0 flex-1 items-center gap-1.5 text-left" @click="onRowClick($event, s)">
|
||||
<SessionStateBadge :session="s" />
|
||||
<span class="truncate font-mono" :class="s.live ? '' : 'text-fg-subtle'">{{ sessionLabel(s, worktrees) }}</span>
|
||||
<BaseBadge v-if="s.source === 'discovered'" tone="sky" class="shrink-0">{{ t('sessions.sourceDiscovered') }}</BaseBadge>
|
||||
<BaseBadge v-if="s.archived" tone="amber" class="shrink-0">{{ t('sessions.archivedBadge') }}</BaseBadge>
|
||||
</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="openRowMenu($event, s)"
|
||||
>
|
||||
<MoreVertical :size="13" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// Panneau Sessions de l'IDE : liste live + morte, supervision « À traiter » en tête, et cycle de
|
||||
// vie complet via menu contextuel (open/observe/kill/resume/fork/hide/archive). Toute action passe
|
||||
// par useSessionActions ou le store sessions ; ouvrir un terminal = ide.openTerminal (aucune
|
||||
// navigation hors IDE).
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { ExternalLink } from '@lucide/vue';
|
||||
import {
|
||||
Plus,
|
||||
MoreHorizontal,
|
||||
MoreVertical,
|
||||
SquareTerminal,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Trash2,
|
||||
GitFork,
|
||||
Play,
|
||||
Archive,
|
||||
ArchiveRestore,
|
||||
RefreshCw,
|
||||
FolderPlus,
|
||||
} 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 { useModalsStore } from '../../stores/modals';
|
||||
import { useContextMenu, type ContextMenuItem } from '../../composables/useContextMenu';
|
||||
import { useSessionActions } from '../../composables/useSessionActions';
|
||||
import { sessionLabel } from '../../lib/session-label';
|
||||
import SessionStateBadge from '../SessionStateBadge.vue';
|
||||
import BaseBadge from '../ui/BaseBadge.vue';
|
||||
import AttentionList from './AttentionList.vue';
|
||||
import ConfirmModal from './modals/ConfirmModal.vue';
|
||||
import NewSessionModal from './modals/NewSessionModal.vue';
|
||||
import NewProjectModal from '../NewProjectModal.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const ide = useIdeStore();
|
||||
const sessions = useSessionsStore();
|
||||
const store = useSessionsStore();
|
||||
const worktrees = useWorktreesStore();
|
||||
const modals = useModalsStore();
|
||||
const ctx = useContextMenu();
|
||||
const actions = useSessionActions();
|
||||
|
||||
const liveSessions = computed(() => sessions.sessions.filter((s) => s.live));
|
||||
const rows = computed(() => store.sessions);
|
||||
const hasDiscovered = computed(() => store.sessions.some((s) => s.source === 'discovered' && !s.hidden));
|
||||
|
||||
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;
|
||||
function onRowClick(e: MouseEvent, s: SessionSummary): void {
|
||||
if (s.live || s.attachable) ide.openTerminal(s.id);
|
||||
else openRowMenu(e, s);
|
||||
}
|
||||
|
||||
function confirmKill(s: SessionSummary): void {
|
||||
modals.open(ConfirmModal, {
|
||||
title: t('sessions.kill'),
|
||||
message: sessionLabel(s, worktrees),
|
||||
danger: true,
|
||||
confirmLabel: t('sessions.confirmKill'),
|
||||
onConfirm: () => actions.kill(s.id),
|
||||
});
|
||||
}
|
||||
|
||||
function menuFor(s: SessionSummary): ContextMenuItem[] {
|
||||
const items: ContextMenuItem[] = [];
|
||||
if (s.attachable) {
|
||||
items.push({ label: t('sessions.open'), icon: SquareTerminal, onSelect: () => ide.openTerminal(s.id) });
|
||||
items.push({ label: t('sessions.kill'), icon: Trash2, danger: true, onSelect: () => confirmKill(s) });
|
||||
} else if (s.live) {
|
||||
items.push({ label: t('sessions.observe'), icon: Eye, onSelect: () => ide.openTerminal(s.id) });
|
||||
items.push({ label: t('sessions.fork'), icon: GitFork, onSelect: () => void actions.fork(s.id) });
|
||||
} else if (s.resumable) {
|
||||
items.push({ label: t('sessions.resume'), icon: Play, onSelect: () => void actions.resume(s.id) });
|
||||
items.push({ label: t('sessions.fork'), icon: GitFork, onSelect: () => void actions.fork(s.id) });
|
||||
}
|
||||
return s.cwd.split('/').filter(Boolean).pop() ?? s.cwd;
|
||||
if (s.hidden) items.push({ label: t('sessions.unhide'), icon: Eye, onSelect: () => void actions.unhide(s.id) });
|
||||
else if (s.source === 'discovered') items.push({ label: t('sessions.hide'), icon: EyeOff, onSelect: () => void actions.hide(s.id) });
|
||||
if (s.archived) items.push({ label: t('sessions.unarchive'), icon: ArchiveRestore, onSelect: () => void actions.unarchive(s.id) });
|
||||
else if (s.source === 'managed' && !s.live) items.push({ label: t('sessions.archive'), icon: Archive, onSelect: () => void actions.archive(s.id) });
|
||||
return items;
|
||||
}
|
||||
|
||||
function openRowMenu(e: MouseEvent, s: SessionSummary): void {
|
||||
const items = menuFor(s);
|
||||
if (items.length) ctx.open(e.clientX, e.clientY, items);
|
||||
}
|
||||
|
||||
function openAddMenu(e: MouseEvent): void {
|
||||
ctx.open(e.clientX, e.clientY, [
|
||||
{ label: t('sessions.newSession'), icon: SquareTerminal, onSelect: () => void modals.open(NewSessionModal) },
|
||||
{ label: t('project.new'), icon: FolderPlus, onSelect: () => void modals.open(NewProjectModal) },
|
||||
]);
|
||||
}
|
||||
|
||||
function openMoreMenu(e: MouseEvent): void {
|
||||
ctx.open(e.clientX, e.clientY, [
|
||||
{
|
||||
label: t(store.showHidden ? 'sessions.hideHidden' : 'sessions.showHidden'),
|
||||
icon: store.showHidden ? EyeOff : Eye,
|
||||
onSelect: () => void store.setShowHidden(!store.showHidden),
|
||||
},
|
||||
{
|
||||
label: t(store.showArchived ? 'sessions.hideArchived' : 'sessions.showArchived'),
|
||||
icon: store.showArchived ? ArchiveRestore : Archive,
|
||||
onSelect: () => void store.setShowArchived(!store.showArchived),
|
||||
},
|
||||
{ label: t('sessions.hideDiscovered'), icon: EyeOff, disabled: !hasDiscovered.value, onSelect: () => void actions.hideDiscovered() },
|
||||
{ label: t('common.refresh'), icon: RefreshCw, onSelect: () => void store.fetchSessions() },
|
||||
]);
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user