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:
@@ -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>
|
||||
Reference in New Issue
Block a user