Projekt-Identität und -Config neu: .ai-control/config.json mit UUID und Name

- Projekt-Config zieht von ai-control.json nach .ai-control/config.json um,
  das Icon liegt daneben und synct mit dem Projekt.
- Jedes Projekt trägt eine UUID (id) und den Anzeigenamen (name) in der
  Config; die UUID ist Registry-Schlüssel und project-Parameter aller
  Commands, der Name reine Darstellung.
- Pool-Zuordnung ist maschinenlokal und liegt in der projects.json-Registry,
  nicht mehr in der syncbaren Projekt-Config.
- open_terminal prüft die ID im Ordner gegen die Registry (verschobene oder
  ersetzte Ordner scheitern laut).
- Import übernimmt eine mitgebrachte ID, legt Arbeitsordner und Archiv-Home
  an und zieht die Archiv-Permission nach.
- Migration beim App-Start: Alt-Datei umziehen, Pool in die Registry,
  Icons aus ~/.config/ai-control/icons ins Projekt, Registry auf UUIDs.
- ProjectConfig reicht unbekannte Keys per serde(flatten) durch den
  Round-Trip (Regressionstest config_roundtrip_erhaelt_fremde_keys).
This commit is contained in:
marcus hinz
2026-07-20 10:55:21 +02:00
parent 6d44f77671
commit b9d006184a
20 changed files with 830 additions and 346 deletions
+7 -6
View File
@@ -6,6 +6,7 @@ import { currentMonitor } from "@tauri-apps/api/window";
import { LogicalSize } from "@tauri-apps/api/dpi";
interface Project {
id: string;
name: string;
running: boolean;
terminal: { theme: string | null; icon: string | null; title: string | null };
@@ -25,9 +26,9 @@ async function refresh() {
try {
projects.value = await invoke<Project[]>("list_projects");
for (const p of projects.value) {
if (p.terminal.icon && !(p.name in icons.value)) {
icons.value[p.name] =
(await invoke<string | null>("project_icon", { project: p.name })) ?? "";
if (p.terminal.icon && !(p.id in icons.value)) {
icons.value[p.id] =
(await invoke<string | null>("project_icon", { project: p.id })) ?? "";
}
}
} catch (e) {
@@ -36,7 +37,7 @@ async function refresh() {
}
async function pick(p: Project) {
await invoke("start_or_focus_cmd", { project: p.name });
await invoke("start_or_focus_cmd", { project: p.id });
await win.hide();
}
async function openMain() {
@@ -72,9 +73,9 @@ onUnmounted(() => {
<template>
<div ref="wrapRef" class="wrap" :style="{ maxHeight: maxH + 'px' }">
<ul class="list">
<li v-for="p in projects" :key="p.name" class="row" @click="pick(p)">
<li v-for="p in projects" :key="p.id" class="row" @click="pick(p)">
<span class="dot" :class="{ on: p.running }"></span>
<img v-if="icons[p.name]" class="ic" :src="icons[p.name]" />
<img v-if="icons[p.id]" class="ic" :src="icons[p.id]" />
<span v-else class="ic ph"></span>
<span class="nm">{{ p.name }}</span>
</li>
+31 -26
View File
@@ -4,6 +4,7 @@ import { invoke } from "@tauri-apps/api/core";
import { open } from "@tauri-apps/plugin-dialog";
interface Project {
id: string;
name: string;
path: string;
pool: string | null;
@@ -44,7 +45,7 @@ const error = ref("");
const icons = ref<Record<string, string>>({});
function iconKey(p: Project): string | null {
return p.terminal.icon ? `${p.name}:${p.terminal.icon}` : null;
return p.terminal.icon ? `${p.id}:${p.terminal.icon}` : null;
}
function projIcon(p: Project): string | undefined {
@@ -58,7 +59,7 @@ async function loadIcons() {
if (!key || key in icons.value) continue;
try {
const data = await invoke<string | null>("project_icon", {
project: p.name,
project: p.id,
});
icons.value[key] = data ?? "";
} catch (e) {
@@ -81,7 +82,7 @@ async function refresh() {
async function stop(project: Project) {
try {
await invoke("stop_project", { project: project.name });
await invoke("stop_project", { project: project.id });
await refresh();
} catch (e) {
error.value = String(e);
@@ -90,7 +91,7 @@ async function stop(project: Project) {
async function openTerminal(project: Project) {
try {
await invoke("open_terminal", { project: project.name });
await invoke("open_terminal", { project: project.id });
await refresh();
} catch (e) {
error.value = String(e);
@@ -98,6 +99,7 @@ async function openTerminal(project: Project) {
}
interface PendingRestart {
id: string;
name: string;
from: string | null;
to: string;
@@ -110,9 +112,9 @@ async function assign(project: Project, event: Event) {
const pool = (event.target as HTMLSelectElement).value;
const from = project.pool;
try {
await invoke("assign_pool", { project: project.name, pool });
await invoke("assign_pool", { project: project.id, pool });
if (project.running && pool !== from) {
pending.value = { name: project.name, from, to: pool };
pending.value = { id: project.id, name: project.name, from, to: pool };
}
await refresh();
} catch (e) {
@@ -124,7 +126,7 @@ async function restartNow() {
const p = pending.value!;
restarting.value = true;
try {
await invoke("restart_project", { project: p.name });
await invoke("restart_project", { project: p.id });
pending.value = null;
await refresh();
} catch (e) {
@@ -151,6 +153,7 @@ const THEME_NAMES: [string, string][] = [
];
interface TerminalSettings {
id: string;
name: string;
path: string;
theme: string;
@@ -165,14 +168,15 @@ const settings = ref<TerminalSettings | null>(null);
async function openSettings(p: Project) {
try {
const todo = await invoke<boolean>("todo_state", { project: p.name });
const todo = await invoke<boolean>("todo_state", { project: p.id });
const workDirs = await invoke<string[]>("project_work_dirs", {
project: p.name,
project: p.id,
});
const archiveHome = await invoke<string | null>("panel_archive_dir_cmd", {
project: p.name,
project: p.id,
});
settings.value = {
id: p.id,
name: p.name,
path: p.path,
theme: p.terminal.theme ?? "mocha",
@@ -194,9 +198,9 @@ async function changeProjectDir() {
const dir = await open({ directory: true, multiple: false });
if (typeof dir !== "string") return;
try {
await invoke("set_project_dir", { project: s.name, dir });
await invoke("set_project_dir", { project: s.id, dir });
await refresh();
const p = projects.value.find((x) => x.name === s.name);
const p = projects.value.find((x) => x.id === s.id);
if (p) s.path = p.path;
} catch (e) {
error.value = String(e);
@@ -210,8 +214,8 @@ async function addWorkDir() {
const dir = await open({ directory: true, multiple: false });
if (typeof dir !== "string") return;
try {
await invoke("add_work_dir", { project: s.name, dir });
s.workDirs = await invoke<string[]>("project_work_dirs", { project: s.name });
await invoke("add_work_dir", { project: s.id, dir });
s.workDirs = await invoke<string[]>("project_work_dirs", { project: s.id });
} catch (e) {
error.value = String(e);
}
@@ -220,8 +224,8 @@ async function addWorkDir() {
async function removeWorkDir(dir: string) {
const s = settings.value!;
try {
await invoke("remove_work_dir", { project: s.name, dir });
s.workDirs = await invoke<string[]>("project_work_dirs", { project: s.name });
await invoke("remove_work_dir", { project: s.id, dir });
s.workDirs = await invoke<string[]>("project_work_dirs", { project: s.id });
} catch (e) {
error.value = String(e);
}
@@ -235,9 +239,9 @@ async function chooseArchive() {
const dir = await open({ directory: true, multiple: false });
if (typeof dir !== "string") return;
try {
await invoke("set_archive_home_cmd", { project: s.name, dir });
await invoke("set_archive_home_cmd", { project: s.id, dir });
s.archiveHome = await invoke<string | null>("panel_archive_dir_cmd", {
project: s.name,
project: s.id,
});
} catch (e) {
error.value = String(e);
@@ -247,7 +251,7 @@ async function chooseArchive() {
async function clearArchive() {
const s = settings.value!;
try {
await invoke("clear_archive_home_cmd", { project: s.name });
await invoke("clear_archive_home_cmd", { project: s.id });
s.archiveHome = null;
} catch (e) {
error.value = String(e);
@@ -268,12 +272,12 @@ async function saveSettings() {
try {
const title = s.title.trim();
await invoke("set_terminal_config", {
project: s.name,
project: s.id,
theme: s.theme === "mocha" ? null : s.theme,
icon: s.icon,
title: title === "" ? null : title,
});
await invoke("set_todo", { project: s.name, enabled: s.todo });
await invoke("set_todo", { project: s.id, enabled: s.todo });
settings.value = null;
await refresh();
} catch (e) {
@@ -371,6 +375,7 @@ async function createProject() {
}
interface PendingDelete {
id: string;
name: string;
workDirs: string[];
deleteWorkDirs: boolean;
@@ -382,9 +387,9 @@ async function askDelete(p: Project) {
error.value = "";
try {
const workDirs = await invoke<string[]>("project_work_dirs", {
project: p.name,
project: p.id,
});
pendingDelete.value = { name: p.name, workDirs, deleteWorkDirs: false };
pendingDelete.value = { id: p.id, name: p.name, workDirs, deleteWorkDirs: false };
} catch (e) {
error.value = String(e);
}
@@ -394,7 +399,7 @@ async function confirmDelete() {
const d = pendingDelete.value!;
try {
await invoke("delete_project", {
name: d.name,
project: d.id,
deleteWorkDirs: d.deleteWorkDirs,
});
pendingDelete.value = null;
@@ -409,7 +414,7 @@ async function confirmDelete() {
async function unlinkProject() {
const d = pendingDelete.value!;
try {
await invoke("remove_project", { name: d.name });
await invoke("remove_project", { project: d.id });
pendingDelete.value = null;
await refresh();
} catch (e) {
@@ -453,7 +458,7 @@ onUnmounted(() => window.clearInterval(timer));
</tr>
</thead>
<tbody>
<tr v-for="p in projects" :key="p.name">
<tr v-for="p in projects" :key="p.id">
<td class="cell-dot">
<span class="dot" :class="{ on: p.running }"></span>
</td>
+3 -2
View File
@@ -69,12 +69,13 @@ document.querySelector("header")!.addEventListener("dblclick", (e) => {
// bis die Webview lädt).
interface Project {
id: string;
name: string;
pool: string | null;
terminal: { theme: string | null; icon: string | null; title: string | null };
}
const projects = await invoke<Project[]>("list_projects");
const cfg = projects.find((p) => p.name === project);
const cfg = projects.find((p) => p.id === project);
if (cfg?.pool) {
// Anzeigename statt Pool-ID (Ordnername).
document.getElementById("pool")!.textContent = await invoke<string>(
@@ -83,7 +84,7 @@ if (cfg?.pool) {
);
}
document.getElementById("project-name")!.textContent =
cfg?.terminal.title ?? project;
cfg?.terminal.title ?? cfg?.name ?? project;
// Projekt-Icon vor den Titel — dieselbe data-URL wie in der Projektliste.
if (cfg?.terminal.icon) {