Projekt löschen in drei Stufen mit Artefakt-Vorschau; Listen-Scroll gefixt

Eskalationsleiter: 'integration' nimmt nur die ai-control-Spuren zurück
(Registry, .ai-control/, Archiv-Rechte + Todo-Hook in settings.json,
Panel-Dateien, .desktop — Ordner, memory/ und Claude-Code-Grundgerüst
bleiben), 'archive' löscht zusätzlich das Archiv, 'full' den Projekt-
ordner mit Arbeitsordner-Flag. delete_preview liefert die konkrete
Artefaktliste; der Dialog zeigt sie je Stufe. Ersetzt delete_project/
remove_project (Handler, App-ACL-Manifest und Capability nachgezogen).

Außerdem: main scrollt nicht mehr — Toolbars und Fehlerzeile stehen,
nur die Listen scrollen (alle drei Tabs, .list-scroll).
This commit is contained in:
marcus hinz
2026-07-20 13:37:02 +02:00
parent 6682040cc0
commit 6c765ff7b5
73 changed files with 1250 additions and 102 deletions
+3 -1
View File
@@ -201,7 +201,8 @@ onMounted(refresh);
<p v-if="error" class="error">{{ error }}</p>
<table v-if="pools.length" class="grid">
<div v-if="pools.length" class="list-scroll">
<table class="grid">
<colgroup>
<col />
<col class="col-type" />
@@ -268,6 +269,7 @@ onMounted(refresh);
</tr>
</tbody>
</table>
</div>
<p v-else class="empty">{{ $t("pools.empty") }}</p>
<div v-if="dialog" class="overlay" @click.self="closeDialog">
+122 -40
View File
@@ -379,10 +379,25 @@ async function createProject() {
}
}
interface DeletePreview {
name: string;
projectDir: string;
aiControlDir: boolean;
archivePermission: boolean;
todoHook: boolean;
panelFiles: number;
archiveHome: string | null;
archiveDocs: number;
workDirs: string[];
}
// Eskalationsleiter: jede Stufe schließt die vorige ein.
type DeleteScope = "integration" | "archive" | "full";
interface PendingDelete {
id: string;
name: string;
workDirs: string[];
preview: DeletePreview;
scope: DeleteScope;
deleteWorkDirs: boolean;
}
@@ -391,10 +406,8 @@ const pendingDelete = ref<PendingDelete | null>(null);
async function askDelete(p: Project) {
error.value = "";
try {
const workDirs = await invoke<string[]>("project_work_dirs", {
project: p.id,
});
pendingDelete.value = { id: p.id, name: p.name, workDirs, deleteWorkDirs: false };
const preview = await invoke<DeletePreview>("delete_preview", { project: p.id });
pendingDelete.value = { id: p.id, preview, scope: "integration", deleteWorkDirs: false };
} catch (e) {
error.value = String(e);
}
@@ -403,9 +416,10 @@ async function askDelete(p: Project) {
async function confirmDelete() {
const d = pendingDelete.value!;
try {
await invoke("delete_project", {
await invoke("delete_project_scoped", {
project: d.id,
deleteWorkDirs: d.deleteWorkDirs,
scope: d.scope,
deleteWorkDirs: d.scope === "full" && d.deleteWorkDirs,
});
pendingDelete.value = null;
await refresh();
@@ -415,19 +429,6 @@ async function confirmDelete() {
}
}
// Nur den Registry-Eintrag entfernen; der Ordner bleibt.
async function unlinkProject() {
const d = pendingDelete.value!;
try {
await invoke("remove_project", { project: d.id });
pendingDelete.value = null;
await refresh();
} catch (e) {
error.value = String(e);
pendingDelete.value = null;
}
}
let timer: number;
onMounted(() => {
refresh();
@@ -447,6 +448,7 @@ onUnmounted(() => window.clearInterval(timer));
</div>
<p v-if="error" class="error">{{ error }}</p>
<div class="list-scroll">
<table class="grid">
<colgroup>
<col class="col-dot" />
@@ -516,6 +518,7 @@ onUnmounted(() => window.clearInterval(timer));
</tr>
</tbody>
</table>
</div>
<div v-if="wizard" class="overlay" @click.self="wizard = null">
<form class="dialog" @submit.prevent="createProject">
@@ -619,30 +622,76 @@ onUnmounted(() => window.clearInterval(timer));
<div v-if="pendingDelete" class="overlay" @click.self="pendingDelete = null">
<div class="dialog">
<h3>{{ $t("projects.deleteTitle", { name: pendingDelete.name }) }}</h3>
<p class="hint">{{ $t("projects.deleteWarning", { name: pendingDelete.name }) }}</p>
<template v-if="pendingDelete.workDirs.length">
<label class="checkline">
<input v-model="pendingDelete.deleteWorkDirs" type="checkbox" />
{{ $t("projects.deleteWorkDirs") }}
</label>
<ul class="affected">
<li v-for="wd in pendingDelete.workDirs" :key="wd">{{ wd }}</li>
</ul>
<p v-if="!pendingDelete.deleteWorkDirs" class="hint">
{{ $t("projects.workDirsStay") }}
</p>
</template>
<p class="hint">{{ $t("projects.unlinkHint") }}</p>
<h3>{{ $t("projects.deleteTitle", { name: pendingDelete.preview.name }) }}</h3>
<label class="scope">
<input v-model="pendingDelete.scope" type="radio" value="integration" />
<span>
<strong>{{ $t("projects.scopeIntegration") }}</strong>
<small>{{ $t("projects.scopeIntegrationDesc") }}</small>
</span>
</label>
<label class="scope">
<input
v-model="pendingDelete.scope"
type="radio"
value="archive"
:disabled="!pendingDelete.preview.archiveHome"
/>
<span>
<strong>{{ $t("projects.scopeArchive") }}</strong>
<small v-if="pendingDelete.preview.archiveHome">
{{ $t("projects.scopeArchiveDesc", { path: pendingDelete.preview.archiveHome }) }}
({{ $t("projects.docCount", pendingDelete.preview.archiveDocs) }})
</small>
<small v-else>{{ $t("projects.archiveNone") }}</small>
</span>
</label>
<label class="scope">
<input v-model="pendingDelete.scope" type="radio" value="full" />
<span>
<strong>{{ $t("projects.scopeFull") }}</strong>
<small>{{ $t("projects.scopeFullDesc", { path: pendingDelete.preview.projectDir }) }}</small>
</span>
</label>
<label
v-if="pendingDelete.scope === 'full' && pendingDelete.preview.workDirs.length"
class="checkline"
>
<input v-model="pendingDelete.deleteWorkDirs" type="checkbox" />
{{ $t("projects.deleteWorkDirs") }}
</label>
<p class="hint">{{ $t("projects.deletePreviewTitle") }}</p>
<ul class="affected">
<li>{{ $t("projects.artRegistry") }}</li>
<li v-if="pendingDelete.preview.aiControlDir">{{ $t("projects.artAiControl") }}</li>
<li v-if="pendingDelete.preview.archivePermission">{{ $t("projects.artArchivePerm") }}</li>
<li v-if="pendingDelete.preview.todoHook">{{ $t("projects.artTodoHook") }}</li>
<li v-if="pendingDelete.preview.panelFiles">
{{ $t("projects.artPanelFiles", pendingDelete.preview.panelFiles) }}
</li>
<li>{{ $t("projects.artDesktop") }}</li>
<li v-if="pendingDelete.scope !== 'integration' && pendingDelete.preview.archiveHome">
{{ $t("projects.artArchive", { path: pendingDelete.preview.archiveHome }) }}
({{ $t("projects.docCount", pendingDelete.preview.archiveDocs) }})
</li>
<li v-if="pendingDelete.scope === 'full'">
{{ $t("projects.artProjectDir", { path: pendingDelete.preview.projectDir }) }}
</li>
<template v-if="pendingDelete.scope === 'full' && pendingDelete.deleteWorkDirs">
<li v-for="wd in pendingDelete.preview.workDirs" :key="wd">
{{ $t("projects.artWorkDir", { path: wd }) }}
</li>
</template>
</ul>
<div class="actions">
<button type="button" @click="pendingDelete = null">
{{ $t("projects.cancel") }}
</button>
<button @click="unlinkProject">
{{ $t("projects.unlink") }}
</button>
<button class="danger" @click="confirmDelete">
{{ $t("projects.delete") }}
{{ $t("projects.deleteConfirm") }}
</button>
</div>
</div>
@@ -784,6 +833,39 @@ onUnmounted(() => window.clearInterval(timer));
</template>
<style scoped>
/* Lösch-Dialog: drei Stufen als Radio-Zeilen mit Beschreibung. */
.scope {
display: flex;
align-items: flex-start;
gap: 0.6rem;
cursor: pointer;
}
.scope input {
margin-top: 0.2rem;
}
.scope span {
display: flex;
flex-direction: column;
gap: 0.1rem;
}
.scope strong {
font-size: 0.9rem;
color: var(--text);
}
.scope small {
color: var(--overlay);
font-size: 0.78rem;
}
.scope:has(input:disabled) {
opacity: 0.55;
cursor: default;
}
/* Settings-Dialog: festes Grid Label | Wert | Aktionen in drei Gruppen. */
.settings-dialog {
position: relative;
+3 -1
View File
@@ -113,7 +113,8 @@ function cost(n: number): string {
<p v-if="error" class="error">{{ error }}</p>
<table v-if="groups.length" class="grid">
<div v-if="groups.length" class="list-scroll">
<table class="grid">
<colgroup>
<col />
<col class="col-num" />
@@ -164,5 +165,6 @@ function cost(n: number): string {
</tr>
</tfoot>
</table>
</div>
<p v-else-if="!loading" class="empty">{{ $t("usage.empty") }}</p>
</template>
+38 -12
View File
@@ -71,13 +71,26 @@ const de = {
delete: "Projekt löschen",
deleteRunning: "Läuft noch — erst beenden",
deleteTitle: "{name} löschen",
deleteWarning:
"Der Projektordner (inkl. memory/, Settings und Pool-Zuordnung) wird endgültig gelöscht.",
deleteConfirm: "Entfernen",
deleteWorkDirs: "Arbeitsordner mitlöschen:",
workDirsStay: "Die Arbeitsordner bleiben unangetastet.",
unlink: "Nur aus Liste nehmen",
unlinkHint:
"Nur aus Liste nehmen entfernt den Eintrag; der Ordner bleibt unangetastet.",
scopeIntegration: "Nur Integration entfernen",
scopeIntegrationDesc:
"ai-control-Spuren werden entfernt; Projektordner, memory/ und Todoliste bleiben.",
scopeArchive: "Integration & Archiv",
scopeArchiveDesc: "zusätzlich {path} — endgültig.",
scopeFull: "Projekt komplett löschen",
scopeFullDesc: "zusätzlich der Projektordner {path} — endgültig.",
deletePreviewTitle: "Wird entfernt:",
docCount: "kein Dokument | 1 Dokument | {count} Dokumente",
artRegistry: "Registry-Eintrag",
artAiControl: ".ai-control/ (Config + Icon)",
artArchivePerm: "Archiv-Berechtigung in .claude/settings.json",
artTodoHook: "Todo-Hook in .claude/settings.json",
artPanelFiles: "Panel-Kanaldatei | {count} Panel-Kanaldateien",
artDesktop: ".desktop-Starter",
artArchive: "Archiv {path}",
artProjectDir: "Projektordner {path}",
artWorkDir: "Arbeitsordner {path}",
todo: "Todoliste",
todoDesc: "OFFENE-PUNKTE.md bei jedem Sessionstart einspielen",
groupAppearance: "Darstellung",
@@ -201,13 +214,26 @@ const en: typeof de = {
delete: "Delete project",
deleteRunning: "Still running — stop it first",
deleteTitle: "Delete {name}",
deleteWarning:
"The project folder (incl. memory/, settings and pool assignment) will be deleted permanently.",
deleteConfirm: "Remove",
deleteWorkDirs: "Also delete working folders:",
workDirsStay: "The working folders stay untouched.",
unlink: "Remove from list only",
unlinkHint:
"Remove from list only drops the entry; the folder stays untouched.",
scopeIntegration: "Remove integration only",
scopeIntegrationDesc:
"ai-control traces are removed; project folder, memory/ and todo list stay.",
scopeArchive: "Integration & archive",
scopeArchiveDesc: "additionally {path} — permanent.",
scopeFull: "Delete project completely",
scopeFullDesc: "additionally the project folder {path} — permanent.",
deletePreviewTitle: "Will be removed:",
docCount: "no documents | 1 document | {count} documents",
artRegistry: "registry entry",
artAiControl: ".ai-control/ (config + icon)",
artArchivePerm: "archive permission in .claude/settings.json",
artTodoHook: "todo hook in .claude/settings.json",
artPanelFiles: "panel channel file | {count} panel channel files",
artDesktop: ".desktop launcher",
artArchive: "archive {path}",
artProjectDir: "project folder {path}",
artWorkDir: "working folder {path}",
todo: "Todo list",
todoDesc: "Inject OFFENE-PUNKTE.md at every session start",
groupAppearance: "Appearance",
+11 -1
View File
@@ -123,10 +123,20 @@ input[type="checkbox"] {
main {
flex: 1;
overflow-y: auto;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
padding: 1.5rem;
}
/* Tab-Inhalte: Toolbar und Fehlerzeile stehen, nur die Liste scrollt. */
.list-scroll {
flex: 1;
min-height: 0;
overflow-y: auto;
}
/* ---------- Controls ---------- */
button,