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:
2026-07-20 12:10:12 +02:00
parent e48798ebff
commit c694f9f2dd
57 changed files with 1457 additions and 3045 deletions
@@ -1,146 +0,0 @@
<template>
<div class="flex h-full min-h-0 flex-col">
<!-- barre d'outils de l'éditeur -->
<div class="flex items-center gap-2 border-b border-zinc-800 px-3 py-1 text-xs">
<span class="min-w-0 flex-1 truncate font-mono text-zinc-400" :title="file">{{ file }}</span>
<span v-if="dirty" class="text-amber-400" :title="t('editor.unsaved')">●</span>
<BaseButton size="sm" variant="ghost" :icon="Save" :loading="saving" :disabled="!dirty || !ready" @click="save">
{{ t('editor.save') }}
</BaseButton>
</div>
<!-- bannière de conflit (le fichier a changé sur le disque depuis le chargement) -->
<div v-if="conflict" class="flex flex-wrap items-center gap-2 border-b border-amber-700/40 bg-amber-950/30 px-3 py-1.5 text-xs text-amber-200">
<TriangleAlert :size="14" /> {{ t('editor.conflict') }}
<div class="ml-auto flex items-center gap-2">
<BaseButton size="sm" variant="ghost" @click="reload">{{ t('editor.reload') }}</BaseButton>
<BaseButton size="sm" variant="danger" :loading="saving" @click="overwrite">{{ t('editor.overwrite') }}</BaseButton>
</div>
</div>
<p v-if="loadError" class="px-3 py-2 text-xs text-rose-400">{{ loadError }}</p>
<!-- conteneur Monaco -->
<div ref="host" class="min-h-0 flex-1" />
</div>
</template>
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { Save, TriangleAlert } from '@lucide/vue';
import type * as Monaco from 'monaco-editor';
import { gitApi } from '../../lib/git-api';
import { ApiError } from '../../lib/api';
import { loadMonaco } from '../../composables/useMonaco';
import BaseButton from '../ui/BaseButton.vue';
const props = defineProps<{ repoId: string; wt: string; file: string }>();
const emit = defineEmits<{ saved: [] }>();
const { t } = useI18n();
const host = ref<HTMLElement | null>(null);
const ready = ref(false);
const dirty = ref(false);
const saving = ref(false);
const conflict = ref(false);
const loadError = ref<string | null>(null);
let monaco: typeof Monaco | null = null;
const editor = shallowRef<Monaco.editor.IStandaloneCodeEditor | null>(null);
let model: Monaco.editor.ITextModel | null = null;
let baseMtime: number | undefined;
let savedContent = '';
let disposed = false;
onMounted(async () => {
monaco = await loadMonaco();
if (disposed || !host.value) return;
editor.value = monaco.editor.create(host.value, {
value: '',
theme: 'arboretum-dark',
automaticLayout: true,
minimap: { enabled: false },
wordWrap: 'on',
fontSize: 13,
scrollBeyondLastLine: false,
tabSize: 2,
});
editor.value.onDidChangeModelContent(() => {
dirty.value = editor.value?.getValue() !== savedContent;
});
// Ctrl/Cmd+S : sauvegarde. Monaco n'intercepte que si l'éditeur a le focus → pas de capture globale.
editor.value.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => void save());
ready.value = true;
await loadFile();
});
async function loadFile(): Promise<void> {
if (!monaco || !editor.value) return;
loadError.value = null;
conflict.value = false;
try {
const res = await gitApi.readFile(props.repoId, props.wt, props.file);
if (disposed) return;
baseMtime = res.mtime;
savedContent = res.content;
const next = monaco.editor.createModel(res.content, res.language);
editor.value.setModel(next);
model?.dispose();
model = next;
dirty.value = false;
} catch (err) {
loadError.value = err instanceof Error ? err.message : String(err);
}
}
async function save(): Promise<void> {
if (!editor.value || saving.value || !dirty.value) return;
saving.value = true;
conflict.value = false;
try {
const content = editor.value.getValue();
const res = await gitApi.writeFile(props.repoId, props.wt, props.file, content, baseMtime);
baseMtime = res.mtime;
savedContent = content;
dirty.value = false;
emit('saved');
} catch (err) {
if (err instanceof ApiError && err.code === 'STALE_FILE') conflict.value = true;
else loadError.value = err instanceof Error ? err.message : String(err);
} finally {
saving.value = false;
}
}
// Écrase malgré le conflit (PUT sans baseMtime → le serveur ne vérifie plus la fraîcheur).
async function overwrite(): Promise<void> {
if (!editor.value || saving.value) return;
saving.value = true;
try {
const content = editor.value.getValue();
const res = await gitApi.writeFile(props.repoId, props.wt, props.file, content);
baseMtime = res.mtime;
savedContent = content;
dirty.value = false;
conflict.value = false;
emit('saved');
} catch (err) {
loadError.value = err instanceof Error ? err.message : String(err);
} finally {
saving.value = false;
}
}
// Recharge depuis le disque (abandonne les modifications locales).
async function reload(): Promise<void> {
await loadFile();
}
watch(() => [props.repoId, props.wt, props.file], () => void loadFile());
onBeforeUnmount(() => {
disposed = true;
model?.dispose();
editor.value?.dispose();
});
</script>