Projekt-Wizard und Projekt löschen
- create_project_full: Scaffold nach 08-07-Muster (memory/, Sentinel- .gitignore, settings.json mit autoMemoryDirectory/Permissions/pool-guard- Hook, ai-control.json mit Pool/Terminal); Arbeitsordner anlegen oder vorhandenen verknuepfen, Pruefung vor dem Anlegen; ersetzt create_project - Wizard-Dialog: Name, Pool, Arbeitsordner (keiner/anlegen/Picker), Terminal-Titel + Theme - delete_project: Guard bei laufender Session, optional Arbeitsordner mitloeschen (additionalDirectories, via project_work_dirs); Papierkorb- Button mit Bestaetigungs-Dialog inkl. Checkbox und Pfadliste - 8 neue Tests (36 gesamt), i18n de/en
This commit is contained in:
@@ -149,6 +149,101 @@ async function saveSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
interface Wizard {
|
||||
name: string;
|
||||
pool: string;
|
||||
workDirMode: "none" | "default" | "custom";
|
||||
workDir: string | null;
|
||||
title: string;
|
||||
theme: string;
|
||||
}
|
||||
|
||||
const wizard = ref<Wizard | null>(null);
|
||||
|
||||
function openWizard() {
|
||||
error.value = "";
|
||||
wizard.value = {
|
||||
name: "",
|
||||
pool: "",
|
||||
workDirMode: "none",
|
||||
workDir: null,
|
||||
title: "",
|
||||
theme: "mocha",
|
||||
};
|
||||
}
|
||||
|
||||
async function pickWorkDir() {
|
||||
const dir = await open({ directory: true, multiple: false });
|
||||
if (typeof dir === "string") {
|
||||
wizard.value!.workDir = dir;
|
||||
wizard.value!.workDirMode = "custom";
|
||||
}
|
||||
}
|
||||
|
||||
async function createProject() {
|
||||
const w = wizard.value!;
|
||||
const name = w.name.trim();
|
||||
const workDir =
|
||||
w.workDirMode === "default"
|
||||
? `~/projects/${name}`
|
||||
: w.workDirMode === "custom"
|
||||
? w.workDir
|
||||
: null;
|
||||
const title = w.title.trim();
|
||||
try {
|
||||
await invoke("create_project_full", {
|
||||
name,
|
||||
pool: w.pool === "" ? null : w.pool,
|
||||
workDir,
|
||||
createWorkDir: w.workDirMode === "default",
|
||||
terminal: {
|
||||
theme: w.theme === "mocha" ? null : w.theme,
|
||||
icon: null,
|
||||
title: title === "" ? null : title,
|
||||
},
|
||||
});
|
||||
wizard.value = null;
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
error.value = String(e);
|
||||
}
|
||||
}
|
||||
|
||||
interface PendingDelete {
|
||||
name: string;
|
||||
workDirs: string[];
|
||||
deleteWorkDirs: boolean;
|
||||
}
|
||||
|
||||
const pendingDelete = ref<PendingDelete | null>(null);
|
||||
|
||||
async function askDelete(p: Project) {
|
||||
error.value = "";
|
||||
try {
|
||||
const workDirs = await invoke<string[]>("project_work_dirs", {
|
||||
project: p.name,
|
||||
});
|
||||
pendingDelete.value = { name: p.name, workDirs, deleteWorkDirs: false };
|
||||
} catch (e) {
|
||||
error.value = String(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmDelete() {
|
||||
const d = pendingDelete.value!;
|
||||
try {
|
||||
await invoke("delete_project", {
|
||||
name: d.name,
|
||||
deleteWorkDirs: d.deleteWorkDirs,
|
||||
});
|
||||
pendingDelete.value = null;
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
error.value = String(e);
|
||||
pendingDelete.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
let timer: number;
|
||||
onMounted(() => {
|
||||
refresh();
|
||||
@@ -158,6 +253,12 @@ onUnmounted(() => window.clearInterval(timer));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="toolbar">
|
||||
<button class="primary" @click="openWizard">
|
||||
{{ $t("projects.newProject") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
<table class="grid">
|
||||
<colgroup>
|
||||
@@ -200,6 +301,14 @@ onUnmounted(() => window.clearInterval(timer));
|
||||
>
|
||||
⚙︎
|
||||
</button>
|
||||
<button
|
||||
class="gear danger"
|
||||
:disabled="p.running"
|
||||
:title="p.running ? $t('projects.deleteRunning') : $t('projects.delete')"
|
||||
@click="askDelete(p)"
|
||||
>
|
||||
🗑
|
||||
</button>
|
||||
<button v-if="p.running" class="stop" @click="stop(p)">
|
||||
{{ $t("projects.stop") }}
|
||||
</button>
|
||||
@@ -218,6 +327,103 @@ onUnmounted(() => window.clearInterval(timer));
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div v-if="wizard" class="overlay" @click.self="wizard = null">
|
||||
<form class="dialog" @submit.prevent="createProject">
|
||||
<h3>{{ $t("projects.wizardTitle") }}</h3>
|
||||
<label class="field">
|
||||
{{ $t("projects.name") }}
|
||||
<input v-model="wizard.name" autofocus required />
|
||||
</label>
|
||||
<label class="field">
|
||||
{{ $t("projects.pool") }}
|
||||
<select v-model="wizard.pool">
|
||||
<option value="">{{ $t("projects.noPool") }}</option>
|
||||
<option v-for="pool in pools" :key="pool.name" :value="pool.name">
|
||||
{{ pool.name }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="field">
|
||||
{{ $t("projects.workDir") }}
|
||||
<span class="icon-row">
|
||||
<select v-model="wizard.workDirMode">
|
||||
<option value="none">{{ $t("projects.workDirNone") }}</option>
|
||||
<option value="default">
|
||||
~/projects/{{ wizard.name.trim() || "…" }}
|
||||
</option>
|
||||
<option value="custom">{{ $t("projects.workDirCustom") }}</option>
|
||||
</select>
|
||||
<button
|
||||
v-if="wizard.workDirMode === 'custom'"
|
||||
type="button"
|
||||
@click="pickWorkDir"
|
||||
>
|
||||
{{ $t("projects.chooseFolder") }}
|
||||
</button>
|
||||
</span>
|
||||
</label>
|
||||
<label v-if="wizard.workDirMode === 'custom' && wizard.workDir" class="field">
|
||||
<span></span>
|
||||
<span class="icon-path">{{ wizard.workDir }}</span>
|
||||
</label>
|
||||
<label class="field">
|
||||
{{ $t("projects.title") }}
|
||||
<input v-model="wizard.title" :placeholder="wizard.name.trim()" />
|
||||
</label>
|
||||
<label class="field">
|
||||
{{ $t("projects.theme") }}
|
||||
<select v-model="wizard.theme">
|
||||
<option v-for="[value, label] in THEME_NAMES" :key="value" :value="value">
|
||||
{{ label }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<p class="hint">{{ $t("projects.wizardHint") }}</p>
|
||||
<div class="actions">
|
||||
<button type="button" @click="wizard = null">
|
||||
{{ $t("projects.cancel") }}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="primary"
|
||||
:disabled="
|
||||
wizard.name.trim() === '' ||
|
||||
(wizard.workDirMode === 'custom' && !wizard.workDir)
|
||||
"
|
||||
>
|
||||
{{ $t("projects.create") }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<div class="actions">
|
||||
<button type="button" @click="pendingDelete = null">
|
||||
{{ $t("projects.cancel") }}
|
||||
</button>
|
||||
<button class="danger" @click="confirmDelete">
|
||||
{{ $t("projects.delete") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="settings" class="overlay">
|
||||
<div class="dialog">
|
||||
<h3>{{ $t("projects.terminal", { name: settings.name }) }}</h3>
|
||||
|
||||
+34
@@ -50,6 +50,23 @@ const de = {
|
||||
noPoolAssigned: "kein Pool",
|
||||
needsPool: "Kein Pool zugeordnet",
|
||||
needsKey: "Pool hat keinen API-Key",
|
||||
newProject: "+ Projekt",
|
||||
wizardTitle: "Neues Projekt",
|
||||
name: "Name",
|
||||
workDir: "Arbeitsordner",
|
||||
workDirNone: "keiner",
|
||||
workDirCustom: "vorhandenen Ordner verknüpfen",
|
||||
chooseFolder: "Ordner wählen …",
|
||||
create: "Anlegen",
|
||||
wizardHint:
|
||||
"Angelegt werden Projektordner mit memory/, Sentinel-.gitignore, settings.json mit pool-guard-Hook und die Pool-/Terminal-Zuordnung.",
|
||||
delete: "Projekt löschen",
|
||||
deleteRunning: "Läuft noch — erst beenden",
|
||||
deleteTitle: "{name} löschen",
|
||||
deleteWarning:
|
||||
"Der Projektordner unter ~/claude-projects (inkl. memory/, Settings und Pool-Zuordnung) wird endgültig gelöscht.",
|
||||
deleteWorkDirs: "Arbeitsordner mitlöschen:",
|
||||
workDirsStay: "Die Arbeitsordner bleiben unangetastet.",
|
||||
},
|
||||
pools: {
|
||||
pool: "Pool",
|
||||
@@ -138,6 +155,23 @@ const en: typeof de = {
|
||||
noPoolAssigned: "no pool",
|
||||
needsPool: "No pool assigned",
|
||||
needsKey: "Pool has no API key",
|
||||
newProject: "+ Project",
|
||||
wizardTitle: "New project",
|
||||
name: "Name",
|
||||
workDir: "Working folder",
|
||||
workDirNone: "none",
|
||||
workDirCustom: "link existing folder",
|
||||
chooseFolder: "Choose folder …",
|
||||
create: "Create",
|
||||
wizardHint:
|
||||
"Creates the project folder with memory/, sentinel .gitignore, settings.json with the pool-guard hook, and the pool/terminal assignment.",
|
||||
delete: "Delete project",
|
||||
deleteRunning: "Still running — stop it first",
|
||||
deleteTitle: "Delete {name}",
|
||||
deleteWarning:
|
||||
"The project folder under ~/claude-projects (incl. memory/, settings and pool assignment) will be deleted permanently.",
|
||||
deleteWorkDirs: "Also delete working folders:",
|
||||
workDirsStay: "The working folders stay untouched.",
|
||||
},
|
||||
pools: {
|
||||
pool: "Pool",
|
||||
|
||||
@@ -576,6 +576,14 @@ td select {
|
||||
color: var(--overlay);
|
||||
}
|
||||
|
||||
.dialog .checkline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dialog .affected {
|
||||
margin: 0;
|
||||
padding-left: 1.1rem;
|
||||
|
||||
Reference in New Issue
Block a user