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