Rust grün (41 Tests, inkl. zwei neuer Rename-Tests). Jetzt das Frontend:


1	<script setup lang="ts">
2	import { onMounted, onUnmounted, ref } from "vue";
3	import { invoke } from "@tauri-apps/api/core";
4	import { open } from "@tauri-apps/plugin-dialog";
5
6	interface Project {
7	  name: string;
8	  path: string;
9	  pool: string | null;
10	  running: boolean;
11	  terminal: { theme: string | null; icon: string | null; title: string | null };
12	}
13
14	interface Pool {
15	  name: string;
16	  credentialType: string;
17	  hasCredentials: boolean;
18	}
19
20	function poolReady(p: Project): boolean {
21	  if (!p.pool) return false;
22	  const pool = pools.value.find((x) => x.name === p.pool);
23	  return pool !== undefined && pool.hasCredentials;
24	}
25
26	function startBlockedReason(p: Project): string | undefined {
27	  if (poolReady(p)) return undefined;
28	  return p.pool ? "projects.needsKey" : "projects.needsPool";
29	}
30
31	const projects = ref<Project[]>([]);
32	const pools = ref<Pool[]>([]);
33	const error = ref("");
34
35	// data-URLs pro (Projekt, Icon-Pfad) — Fehlversuche werden als "" gemerkt,
36	// damit der 3s-Poll nicht dieselbe Fehlermeldung wiederholt.
37	const icons = ref<Record<string, string>>({});
38
39	function iconKey(p: Project): string | null {
40	  return p.terminal.icon ? `${p.name}:${p.terminal.icon}` : null;
41	}
42
43	function projIcon(p: Project): string | undefined {
44	  const key = iconKey(p);
45	  return key ? icons.value[key] || undefined : undefined;
46	}
47
48	async function loadIcons() {
49	  for (const p of projects.value) {
50	    const key = iconKey(p);
51	    if (!key || key in icons.value) continue;
52	    try {
53	      const data = await invoke<string | null>("project_icon", {
54	        project: p.name,
55	      });
56	      icons.value[key] = data ?? "";
57	    } catch (e) {
58	      error.value = String(e);
59	      icons.value[key] = "";
60	    }
61	  }
62	}
63
64	async function refresh() {
65	  try {
66	    projects.value = await invoke<Project[]>("list_projects");
67	    pools.value = await invoke<Pool[]>("list_pools");
68	    error.value = "";
69	  } catch (e) {
70	    error.value = String(e);
71	  }
72	  await loadIcons();
73	}
74
75	async function stop(project: Project) {
76	  try {
77	    await invoke("stop_project", { project: project.name });
78	    await refresh();
79	  } catch (e) {
80	    error.value = String(e);
81	  }
82	}
83
84	async function openTerminal(project: Project) {
85	  try {
86	    await invoke("open_terminal", { project: project.name });
87	    await refresh();
88	  } catch (e) {
89	    error.value = String(e);
90	  }
91	}
92
93	interface PendingRestart {
94	  name: string;
95	  from: string | null;
96	  to: string;
97	}
98
99	const pending = ref<PendingRestart | null>(null);
100	const restarting = ref(false);
101
102	async function assign(project: Project, event: Event) {
103	  const pool = (event.target as HTMLSelectElement).value;
104	  const from = project.pool;
105	  try {
106	    await invoke("assign_pool", { project: project.name, pool });
107	    if (project.running && pool !== from) {
108	      pending.value = { name: project.name, from, to: pool };
109	    }
110	    await refresh();
111	  } catch (e) {
112	    error.value = String(e);
113	  }
114	}
115
116	async function restartNow() {
117	  const p = pending.value!;
118	  restarting.value = true;
119	  try {
120	    await invoke("restart_project", { project: p.name });
121	    pending.value = null;
122	    await refresh();
123	  } catch (e) {
124	    error.value = String(e);
125	    pending.value = null;
126	  }
127	  restarting.value = false;
128	}
129
130	const THEME_NAMES: [string, string][] = [

<script setup lang="ts">
import { onMounted, onUnmounted, ref } from "vue";
import { invoke } from "@tauri-apps/api/core";
import { open } from "@tauri-apps/plugin-dialog";

interface Project {
  name: string;
  path: string;
  pool: string | null;
  running: boolean;
  terminal: { theme: string | null; icon: string | null; title: string | null };
}

interface Pool {
  name: string;
  credentialType: string;
  hasCredentials: boolean;
}

function poolReady(p: Project): boolean {
  if (!p.pool) return false;
  const pool = pools.value.find((x) => x.name === p.pool);
  return pool !== undefined && pool.hasCredentials;
}

function startBlockedReason(p: Project): string | undefined {
  if (poolReady(p)) return undefined;
  return p.pool ? "projects.needsKey" : "projects.needsPool";
}

const projects = ref<Project[]>([]);
const pools = ref<Pool[]>([]);
const error = ref("");

// data-URLs pro (Projekt, Icon-Pfad) — Fehlversuche werden als "" gemerkt,
// damit der 3s-Poll nicht dieselbe Fehlermeldung wiederholt.
const icons = ref<Record<string, string>>({});

function iconKey(p: Project): string | null {
  return p.terminal.icon ? `${p.name}:${p.terminal.icon}` : null;
}

function projIcon(p: Project): string | undefined {
  const key = iconKey(p);
  return key ? icons.value[key] || undefined : undefined;
}

async function loadIcons() {
  for (const p of projects.value) {
    const key = iconKey(p);
    if (!key || key in icons.value) continue;
    try {
      const data = await invoke<string | null>("project_icon", {
        project: p.name,
      });
      icons.value[key] = data ?? "";
    } catch (e) {
      error.value = String(e);
      icons.value[key] = "";
    }
  }
}

async function refresh() {
  try {
    projects.value = await invoke<Project[]>("list_projects");
    pools.value = await invoke<Pool[]>("list_pools");
    error.value = "";
  } catch (e) {
    error.value = String(e);
  }
  await loadIcons();
}

async function stop(project: Project) {
  try {
    await invoke("stop_project", { project: project.name });
    await refresh();
  } catch (e) {
    error.value = String(e);
  }
}

async function openTerminal(project: Project) {
  try {
    await invoke("open_terminal", { project: project.name });
    await refresh();
  } catch (e) {
    error.value = String(e);
  }
}

interface PendingRestart {
  name: string;
  from: string | null;
  to: string;
}

const pending = ref<PendingRestart | null>(null);
const restarting = ref(false);

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 });
    if (project.running && pool !== from) {
      pending.value = { name: project.name, from, to: pool };
    }
    await refresh();
  } catch (e) {
    error.value = String(e);
  }
}

async function restartNow() {
  const p = pending.value!;
  restarting.value = true;
  try {
    await invoke("restart_project", { project: p.name });
    pending.value = null;
    await refresh();
  } catch (e) {
    error.value = String(e);
    pending.value = null;
  }
  restarting.value = false;
}

const THEME_NAMES: [string, string][] = [


Benenne Pool privateDefault in private um


190	    await invoke("set_todo", { project: s.name, enabled: s.todo });
191	    settings.value = null;
192	    await refresh();
193	  } catch (e) {
194	    error.value = String(e);
195	  }
196	}
197
198	interface Wizard {
199	  name: string;
200	  pool: string;
201	  workDirMode: "none" | "default" | "custom";
202	  workDir: string | null;
203	  title: string;
204	  theme: string;
205	  todo: boolean;
206	}
207
208	const wizard = ref<Wizard | null>(null);
209
210	function openWizard() {
211	  error.value = "";
212	  wizard.value = {
213	    name: "",
214	    pool: "",
215	    workDirMode: "none",
216	    workDir: null,
217	    title: "",
218	    theme: "mocha",
219	    todo: false,
220	  };
221	}
222
223	async function pickWorkDir() {
224	  const dir = await open({ directory: true, multiple: false });
225	  if (typeof dir === "string") {
226	    wizard.value!.workDir = dir;
227	    wizard.value!.workDirMode = "custom";
228	  }
229	}
230
231	async function createProject() {
232	  const w = wizard.value!;
233	  const name = w.name.trim();
234	  const workDir =
235	    w.workDirMode === "default"
236	      ? `~/projects/${name}`
237	      : w.workDirMode === "custom"
238	        ? w.workDir
239	        : null;
240	  const title = w.title.trim();
241	  try {
242	    await invoke("create_project_full", {
243	      name,
244	      pool: w.pool === "" ? null : w.pool,
245	      workDir,
246	      createWorkDir: w.workDirMode === "default",
247	      terminal: {
248	        theme: w.theme === "mocha" ? null : w.theme,
249	        icon: null,
250	        title: title === "" ? null : title,
251	      },
252	      todo: w.todo,
253	    });
254	    wizard.value = null;
255	    await refresh();
256	  } catch (e) {
257	    error.value = String(e);
258	  }
259	}

    await invoke("set_todo", { project: s.name, enabled: s.todo });
    settings.value = null;
    await refresh();
  } catch (e) {
    error.value = String(e);
  }
}

interface Wizard {
  name: string;
  pool: string;
  workDirMode: "none" | "default" | "custom";
  workDir: string | null;
  title: string;
  theme: string;
  todo: boolean;
}

const wizard = ref<Wizard | null>(null);

function openWizard() {
  error.value = "";
  wizard.value = {
    name: "",
    pool: "",
    workDirMode: "none",
    workDir: null,
    title: "",
    theme: "mocha",
    todo: false,
  };
}

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,
      },
      todo: w.todo,
    });
    wizard.value = null;
    await refresh();
  } catch (e) {
    error.value = String(e);
  }
}


525	        <button class="primary" @click="saveSettings">
526	          {{ $t("projects.save") }}
527	        </button>
528

529

530

531
532

533

534
{{ $t("projects.stillRunning", { name: pending.name }) }}

535
{{ $t("projects.poolChangeSaved") }}

536

537	        {{ pending.from ?? $t("projects.noPoolAssigned") }}
538	        →
539	        {{ pending.to }}
540

541
{{ $t("projects.restartHint") }}

542

543	        <button @click="pending = null">{{ $t("projects.keepRunning") }}</button>
544	        <button class="primary" :disabled="restarting" @click="restartNow">
545	          {{ restarting ? $t("projects.restarting") : $t("projects.restartNow") }}
546	        </button>
547

548

549

550	</template>

<button class="primary" @click="saveSettings">
          {{ $t("projects.save") }}
        </button>

{{ $t("projects.stillRunning", { name: pending.name }) }}

{{ $t("projects.poolChangeSaved") }}

        {{ pending.from ?? $t("projects.noPoolAssigned") }}
        →
        {{ pending.to }}

{{ $t("projects.restartHint") }}

        <button @click="pending = null">{{ $t("projects.keepRunning") }}</button>
        <button class="primary" :disabled="restarting" @click="restartNow">
          {{ restarting ? $t("projects.restarting") : $t("projects.restartNow") }}
        </button>

</template>


Edit

interface Pool {
  name: string;
  credentialType: string;
  hasCredentials: boolean;
}

function poolReady(p: Project): boolean {
  if (!p.pool) return false;
  const pool = pools.value.find((x) => x.name === p.pool);
  return pool !== undefined && pool.hasCredentials;
}

interface Pool {
  id: string;
  name: string;
  credentialType: string;
  hasCredentials: boolean;
}

function poolReady(p: Project): boolean {
  if (!p.pool) return false;
  const pool = pools.value.find((x) => x.id === p.pool);
  return pool !== undefined && pool.hasCredentials;
}

// Projekt-Configs tragen die Pool-ID; angezeigt wird der Name aus pool.json.
function poolName(id: string | null): string | null {
  if (!id) return null;
  return pools.value.find((x) => x.id === id)?.name ?? id;
}


The file /Users/marcus.hinz/projects/ai-control/src/components/ProjectList.vue has been updated successfully. (file state is current in your context — no need to Read it back)

interface Pool {
  name: string;
  credentialType: string;
  hasCredentials: boolean;
}

function poolReady(p: Project): boolean {
  if (!p.pool) return false;
  const pool = pools.value.find((x) => x.name === p.pool);
  return pool !== undefined && pool.hasCredentials;
}

interface Pool {
  id: string;
  name: string;
  credentialType: string;
  hasCredentials: boolean;
}

function poolReady(p: Project): boolean {
  if (!p.pool) return false;
  const pool = pools.value.find((x) => x.id === p.pool);
  return pool !== undefined && pool.hasCredentials;
}

// Projekt-Configs tragen die Pool-ID; angezeigt wird der Name aus pool.json.
function poolName(id: string | null): string | null {
  if (!id) return null;
  return pools.value.find((x) => x.id === id)?.name ?? id;
}

}
interface Pool {
+  id: string;
name: string;
credentialType: string;
hasCredentials: boolean;

-  const pool = pools.value.find((x) => x.name === p.pool);

+  const pool = pools.value.find((x) => x.id === p.pool);

+// Projekt-Configs tragen die Pool-ID; angezeigt wird der Name aus pool.json.

+function poolName(id: string | null): string | null {

+  return pools.value.find((x) => x.id === id)?.name ?? id;

function startBlockedReason(p: Project): string | undefined {

return p.pool ? "projects.needsKey" : "projects.needsPool";


          <select :value="p.pool ?? ''" @change="assign(p, $event)">
            <option value="" disabled>{{ $t("projects.noPool") }}</option>
            <option v-for="pool in pools" :key="pool.name" :value="pool.name">
              {{ pool.name }}

          <select :value="p.pool ?? ''" @change="assign(p, $event)">
            <option value="" disabled>{{ $t("projects.noPool") }}</option>
            <option v-for="pool in pools" :key="pool.id" :value="pool.id">
              {{ pool.name }}


          <select :value="p.pool ?? ''" @change="assign(p, $event)">
            <option value="" disabled>{{ $t("projects.noPool") }}</option>
            <option v-for="pool in pools" :key="pool.name" :value="pool.name">
              {{ pool.name }}

          <select :value="p.pool ?? ''" @change="assign(p, $event)">
            <option value="" disabled>{{ $t("projects.noPool") }}</option>
            <option v-for="pool in pools" :key="pool.id" :value="pool.id">
              {{ pool.name }}

-            <option v-for="pool in pools" :key="pool.name" :value="pool.name">

+            <option v-for="pool in pools" :key="pool.id" :value="pool.id">


        <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 }}

        <select v-model="wizard.pool">
          <option value="">{{ $t("projects.noPool") }}</option>
          <option v-for="pool in pools" :key="pool.id" :value="pool.id">
            {{ pool.name }}


        <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 }}

        <select v-model="wizard.pool">
          <option value="">{{ $t("projects.noPool") }}</option>
          <option v-for="pool in pools" :key="pool.id" :value="pool.id">
            {{ pool.name }}

-          <option v-for="pool in pools" :key="pool.name" :value="pool.name">

+          <option v-for="pool in pools" :key="pool.id" :value="pool.id">


{{ pending.from ?? $t("projects.noPoolAssigned") }}
        →
        {{ pending.to }}

{{ poolName(pending.from) ?? $t("projects.noPoolAssigned") }}
        →
        {{ poolName(pending.to) }}


{{ pending.from ?? $t("projects.noPoolAssigned") }}
        →
        {{ pending.to }}

{{ poolName(pending.from) ?? $t("projects.noPoolAssigned") }}
        →
        {{ poolName(pending.to) }}

{{ $t("projects.stillRunning", { name: pending.name }) }}

-        {{ pending.from ?? $t("projects.noPoolAssigned") }}

+        {{ poolName(pending.from) ?? $t("projects.noPoolAssigned") }}


interface Pool {
  name: string;
  credentialType: string;
  projects: string[];
  running: string[];
  hasCredentials: boolean;
}

interface Pool {
  id: string;
  name: string;
  credentialType: string;
  projects: string[];
  running: string[];
  hasCredentials: boolean;
}


The file /Users/marcus.hinz/projects/ai-control/src/components/PoolList.vue has been updated successfully. (file state is current in your context — no need to Read it back)

interface Pool {
  name: string;
  credentialType: string;
  projects: string[];
  running: string[];
  hasCredentials: boolean;
}

interface Pool {
  id: string;
  name: string;
  credentialType: string;
  projects: string[];
  running: string[];
  hasCredentials: boolean;
}

<template>

    <button class="primary" :disabled="busy" @click="openNew('oauth')">
      {{ $t("pools.newOauth") }}
    </button>
    <button class="primary" :disabled="busy" @click="openNew('apikey')">
      {{ $t("pools.newApikey") }}
    </button>

{{ error }}

    <colgroup>
      <col />
      <col class="col-type" />
      <col class="col-projects" />
      <col class="col-actions-wide" />
    </colgroup>

        {{ $t("pools.pool") }}
        {{ $t("pools.type") }}

          {{ p.name }}

          {{ p.credentialType }}

            {{ $t("pools.assigned", p.projects.length) }}

                {{ name }}

            <template v-if="p.credentialType === 'oauth'">
              <button class="w-action" :disabled="busy" @click="askRelogin(p.name)">
                {{ $t("pools.relogin") }}
              </button>
            </template>
            <template v-else>
              <button class="w-action" :disabled="busy" @click="openEditKey(p.name)">
                {{ p.hasCredentials ? $t("pools.changeKey") : $t("pools.insertKey") }}
              </button>
            </template>

              <button
                class="danger"
                :disabled="busy || p.running.length > 0"
                @click="askDelete(p.name)"
              >
                {{ $t("pools.delete") }}
              </button>

                  {{ $t("pools.deleteBlockedTooltip", { projects: p.running.join(", ") }) }}

{{ $t("pools.empty") }}

    <form class="dialog" @submit.prevent="submit">

{{ dialogTitle }}

        {{ $t("pools.name") }}
        <input v-model="dName" placeholder="z. B. privateDefault" autofocus required />

        {{ $t("pools.apiKey") }}
        <input
          v-model="dKey"
          type="password"
          placeholder="sk-ant-api03-…"
          :autofocus="dialog.editing"
          required
        />

        {{ $t("pools.oauthHint") }}

        <button type="button" :disabled="busy" @click="closeDialog">
          {{ $t("pools.cancel") }}
        </button>
        <button type="submit" class="primary" :disabled="busy">
          {{ dialog.mode === "oauth" ? $t("pools.createPool") : $t("pools.save") }}
        </button>

    </form>

{{ $t("pools.reloginTitle", { name: reloginPool }) }}

      <template v-if="reloginRunning.length">

{{ $t("pools.reloginBlocked") }}

{{ name }}

          <button type="button" @click="reloginPool = null">
            {{ $t("pools.cancel") }}
          </button>

      </template>
      <template v-else>

          {{
            reloginHasEntry
              ? $t("pools.reloginWarning", { name: reloginPool })
              : $t("pools.reloginNoEntry")
          }}

          <button type="button" :disabled="busy" @click="reloginPool = null">
            {{ $t("pools.cancel") }}
          </button>
          <button class="primary" :disabled="busy" @click="confirmRelogin">
            {{ $t("pools.reset") }}
          </button>

      </template>

{{ $t("pools.deletePool") }}

{{ $t("pools.deleteWarning", { name: deleteName }) }}

      <template v-if="deleteProjects.length">

{{ $t("pools.deleteUnassigns") }}

{{ name }}

      </template>

        <button type="button" :disabled="busy" @click="deleteName = null">
          {{ $t("pools.cancel") }}
        </button>
        <button class="danger" :disabled="busy" @click="confirmDelete">
          {{ $t("pools.delete") }}
        </button>

</template>

const { t } = useI18n();
interface Pool {
+  id: string;
name: string;
credentialType: string;
projects: string[];


type Mode = "oauth" | "apikey";
const dialog = ref<{ mode: Mode; editing: boolean } | null>(null);
const dName = ref("");
const dKey = ref("");

type Mode = "oauth" | "apikey";
const dialog = ref<{ mode: Mode; editing: boolean } | null>(null);
const dName = ref("");
const dKey = ref("");
// Ziel-Pool (ID) beim Key-Ändern; bei Neuanlage leer.
const dPool = ref("");


type Mode = "oauth" | "apikey";
const dialog = ref<{ mode: Mode; editing: boolean } | null>(null);
const dName = ref("");
const dKey = ref("");
// Ziel-Pool (ID) beim Key-Ändern; bei Neuanlage leer.
const dPool = ref("");

+// Ziel-Pool (ID) beim Key-Ändern; bei Neuanlage leer.


function openNew(mode: Mode) {
  dName.value = "";
  dKey.value = "";
  error.value = "";
  dialog.value = { mode, editing: false };
}

function openEditKey(pool: string) {
  dName.value = pool;
  dKey.value = "";
  error.value = "";
  dialog.value = { mode: "apikey", editing: true };
}

function openNew(mode: Mode) {
  dName.value = "";
  dKey.value = "";
  dPool.value = "";
  error.value = "";
  dialog.value = { mode, editing: false };
}

function openEditKey(pool: Pool) {
  dName.value = pool.name;
  dKey.value = "";
  dPool.value = pool.id;
  error.value = "";
  dialog.value = { mode: "apikey", editing: true };
}


function openNew(mode: Mode) {
  dName.value = "";
  dKey.value = "";
  error.value = "";
  dialog.value = { mode, editing: false };
}

function openEditKey(pool: string) {
  dName.value = pool;
  dKey.value = "";
  error.value = "";
  dialog.value = { mode: "apikey", editing: true };
}

function openNew(mode: Mode) {
  dName.value = "";
  dKey.value = "";
  dPool.value = "";
  error.value = "";
  dialog.value = { mode, editing: false };
}

function openEditKey(pool: Pool) {
  dName.value = pool.name;
  dKey.value = "";
  dPool.value = pool.id;
  error.value = "";
  dialog.value = { mode: "apikey", editing: true };
}


    if (d.mode === "oauth") {
      await invoke("create_oauth_pool", { name: dName.value });
    } else if (d.editing) {
      await invoke("set_apikey", { pool: dName.value, key: dKey.value });
    } else {

    if (d.mode === "oauth") {
      await invoke("create_oauth_pool", { name: dName.value });
    } else if (d.editing) {
      await invoke("set_apikey", { pool: dPool.value, key: dKey.value });
    } else {


    if (d.mode === "oauth") {
      await invoke("create_oauth_pool", { name: dName.value });
    } else if (d.editing) {
      await invoke("set_apikey", { pool: dName.value, key: dKey.value });
    } else {

    if (d.mode === "oauth") {
      await invoke("create_oauth_pool", { name: dName.value });
    } else if (d.editing) {
      await invoke("set_apikey", { pool: dPool.value, key: dKey.value });
    } else {

-      await invoke("set_apikey", { pool: dName.value, key: dKey.value });

+      await invoke("set_apikey", { pool: dPool.value, key: dKey.value });

await invoke("create_apikey_pool", { name: dName.value, key: dKey.value });


const deleteName = ref<string | null>(null);

const deleteProjects = computed(
  () => pools.value.find((p) => p.name === deleteName.value)?.projects ?? [],
);

function askDelete(pool: string) {
  error.value = "";
  deleteName.value = pool;
}

async function confirmDelete() {
  const name = deleteName.value;
  if (!name) return;
  busy.value = true;
  error.value = "";
  try {
    await invoke("delete_pool", { name });
    deleteName.value = null;
    await refresh();
  } catch (e) {
    error.value = String(e);
  } finally {
    busy.value = false;
  }
}

const deletePool = ref<Pool | null>(null);

function askDelete(pool: Pool) {
  error.value = "";
  deletePool.value = pool;
}

async function confirmDelete() {
  const pool = deletePool.value;
  if (!pool) return;
  busy.value = true;
  error.value = "";
  try {
    await invoke("delete_pool", { pool: pool.id });
    deletePool.value = null;
    await refresh();
  } catch (e) {
    error.value = String(e);
  } finally {
    busy.value = false;
  }
}

const renamePool = ref<Pool | null>(null);
const rName = ref("");

function openRename(pool: Pool) {
  error.value = "";
  rName.value = pool.name;
  renamePool.value = pool;
}

async function confirmRename() {
  const pool = renamePool.value;
  if (!pool) return;
  busy.value = true;
  error.value = "";
  try {
    await invoke("rename_pool", { pool: pool.id, name: rName.value });
    renamePool.value = null;
    await refresh();
  } catch (e) {
    error.value = String(e);
  } finally {
    busy.value = false;
  }
}


const deleteName = ref<string | null>(null);

const deleteProjects = computed(
  () => pools.value.find((p) => p.name === deleteName.value)?.projects ?? [],
);

function askDelete(pool: string) {
  error.value = "";
  deleteName.value = pool;
}

async function confirmDelete() {
  const name = deleteName.value;
  if (!name) return;
  busy.value = true;
  error.value = "";
  try {
    await invoke("delete_pool", { name });
    deleteName.value = null;
    await refresh();
  } catch (e) {
    error.value = String(e);
  } finally {
    busy.value = false;
  }
}

const deletePool = ref<Pool | null>(null);

function askDelete(pool: Pool) {
  error.value = "";
  deletePool.value = pool;
}

async function confirmDelete() {
  const pool = deletePool.value;
  if (!pool) return;
  busy.value = true;
  error.value = "";
  try {
    await invoke("delete_pool", { pool: pool.id });
    deletePool.value = null;
    await refresh();
  } catch (e) {
    error.value = String(e);
  } finally {
    busy.value = false;
  }
}

const renamePool = ref<Pool | null>(null);
const rName = ref("");

function openRename(pool: Pool) {
  error.value = "";
  rName.value = pool.name;
  renamePool.value = pool;
}

async function confirmRename() {
  const pool = renamePool.value;
  if (!pool) return;
  busy.value = true;
  error.value = "";
  try {
    await invoke("rename_pool", { pool: pool.id, name: rName.value });
    renamePool.value = null;
    await refresh();
  } catch (e) {
    error.value = String(e);
  } finally {
    busy.value = false;
  }
}

-const deleteName = ref<string | null>(null);

+const deletePool = ref<Pool | null>(null);

-  () => pools.value.find((p) => p.name === deleteName.value)?.projects ?? [],

-    await invoke("delete_pool", { name });

+    await invoke("delete_pool", { pool: pool.id });

+const renamePool = ref<Pool | null>(null);

+    await invoke("rename_pool", { pool: pool.id, name: rName.value });


const reloginPool = ref<string | null>(null);
const reloginHasEntry = ref(false);
const reloginRunning = ref<string[]>([]);

async function askRelogin(pool: string) {
  error.value = "";
  try {
    reloginHasEntry.value = await invoke<boolean>("keychain_status", { pool });
    const projects = await invoke<Project[]>("list_projects");
    reloginRunning.value = projects
      .filter((p) => p.pool === pool && p.running)
      .map((p) => p.name);
    reloginPool.value = pool;
  } catch (e) {
    error.value = String(e);
  }
}

async function confirmRelogin() {
  const pool = reloginPool.value;
  if (!pool) return;
  busy.value = true;
  error.value = "";
  try {
    await invoke("oauth_login", { pool });
    reloginPool.value = null;
    await refresh();
  } catch (e) {
    error.value = String(e);
    reloginPool.value = null;
  } finally {
    busy.value = false;
  }
}

const reloginPool = ref<Pool | null>(null);
const reloginHasEntry = ref(false);
const reloginRunning = ref<string[]>([]);

async function askRelogin(pool: Pool) {
  error.value = "";
  try {
    reloginHasEntry.value = await invoke<boolean>("keychain_status", { pool: pool.id });
    const projects = await invoke<Project[]>("list_projects");
    reloginRunning.value = projects
      .filter((p) => p.pool === pool.id && p.running)
      .map((p) => p.name);
    reloginPool.value = pool;
  } catch (e) {
    error.value = String(e);
  }
}

async function confirmRelogin() {
  const pool = reloginPool.value;
  if (!pool) return;
  busy.value = true;
  error.value = "";
  try {
    await invoke("oauth_login", { pool: pool.id });
    reloginPool.value = null;
    await refresh();
  } catch (e) {
    error.value = String(e);
    reloginPool.value = null;
  } finally {
    busy.value = false;
  }
}


const reloginPool = ref<string | null>(null);
const reloginHasEntry = ref(false);
const reloginRunning = ref<string[]>([]);

async function askRelogin(pool: string) {
  error.value = "";
  try {
    reloginHasEntry.value = await invoke<boolean>("keychain_status", { pool });
    const projects = await invoke<Project[]>("list_projects");
    reloginRunning.value = projects
      .filter((p) => p.pool === pool && p.running)
      .map((p) => p.name);
    reloginPool.value = pool;
  } catch (e) {
    error.value = String(e);
  }
}

async function confirmRelogin() {
  const pool = reloginPool.value;
  if (!pool) return;
  busy.value = true;
  error.value = "";
  try {
    await invoke("oauth_login", { pool });
    reloginPool.value = null;
    await refresh();
  } catch (e) {
    error.value = String(e);
    reloginPool.value = null;
  } finally {
    busy.value = false;
  }
}

const reloginPool = ref<Pool | null>(null);
const reloginHasEntry = ref(false);
const reloginRunning = ref<string[]>([]);

async function askRelogin(pool: Pool) {
  error.value = "";
  try {
    reloginHasEntry.value = await invoke<boolean>("keychain_status", { pool: pool.id });
    const projects = await invoke<Project[]>("list_projects");
    reloginRunning.value = projects
      .filter((p) => p.pool === pool.id && p.running)
      .map((p) => p.name);
    reloginPool.value = pool;
  } catch (e) {
    error.value = String(e);
  }
}

async function confirmRelogin() {
  const pool = reloginPool.value;
  if (!pool) return;
  busy.value = true;
  error.value = "";
  try {
    await invoke("oauth_login", { pool: pool.id });
    reloginPool.value = null;
    await refresh();
  } catch (e) {
    error.value = String(e);
    reloginPool.value = null;
  } finally {
    busy.value = false;
  }
}

-const reloginPool = ref<string | null>(null);

+const reloginPool = ref<Pool | null>(null);

-async function askRelogin(pool: string) {

+async function askRelogin(pool: Pool) {

-    reloginHasEntry.value = await invoke<boolean>("keychain_status", { pool });

+    reloginHasEntry.value = await invoke<boolean>("keychain_status", { pool: pool.id });

-      .filter((p) => p.pool === pool && p.running)

+      .filter((p) => p.pool === pool.id && p.running)

-    await invoke("oauth_login", { pool });

+    await invoke("oauth_login", { pool: pool.id });


Jetzt das Template von PoolList:


-      <tr v-for="p in pools" :key="p.name">

+      <tr v-for="p in pools" :key="p.id">


            <template v-if="p.credentialType === 'oauth'">
              <button class="w-action" :disabled="busy" @click="askRelogin(p.name)">
                {{ $t("pools.relogin") }}
              </button>
            </template>
            <template v-else>
              <button class="w-action" :disabled="busy" @click="openEditKey(p.name)">
                {{ p.hasCredentials ? $t("pools.changeKey") : $t("pools.insertKey") }}
              </button>
            </template>

              <button
                class="danger"
                :disabled="busy || p.running.length > 0"
                @click="askDelete(p.name)"
              >

            <button :disabled="busy" @click="openRename(p)">
              {{ $t("pools.rename") }}
            </button>
            <template v-if="p.credentialType === 'oauth'">
              <button class="w-action" :disabled="busy" @click="askRelogin(p)">
                {{ $t("pools.relogin") }}
              </button>
            </template>
            <template v-else>
              <button class="w-action" :disabled="busy" @click="openEditKey(p)">
                {{ p.hasCredentials ? $t("pools.changeKey") : $t("pools.insertKey") }}
              </button>
            </template>

              <button
                class="danger"
                :disabled="busy || p.running.length > 0"
                @click="askDelete(p)"
              >


            <template v-if="p.credentialType === 'oauth'">
              <button class="w-action" :disabled="busy" @click="askRelogin(p.name)">
                {{ $t("pools.relogin") }}
              </button>
            </template>
            <template v-else>
              <button class="w-action" :disabled="busy" @click="openEditKey(p.name)">
                {{ p.hasCredentials ? $t("pools.changeKey") : $t("pools.insertKey") }}
              </button>
            </template>

              <button
                class="danger"
                :disabled="busy || p.running.length > 0"
                @click="askDelete(p.name)"
              >

            <button :disabled="busy" @click="openRename(p)">
              {{ $t("pools.rename") }}
            </button>
            <template v-if="p.credentialType === 'oauth'">
              <button class="w-action" :disabled="busy" @click="askRelogin(p)">
                {{ $t("pools.relogin") }}
              </button>
            </template>
            <template v-else>
              <button class="w-action" :disabled="busy" @click="openEditKey(p)">
                {{ p.hasCredentials ? $t("pools.changeKey") : $t("pools.insertKey") }}
              </button>
            </template>

              <button
                class="danger"
                :disabled="busy || p.running.length > 0"
                @click="askDelete(p)"
              >

+            <button :disabled="busy" @click="openRename(p)">

-              <button class="w-action" :disabled="busy" @click="askRelogin(p.name)">

+              <button class="w-action" :disabled="busy" @click="askRelogin(p)">

-              <button class="w-action" :disabled="busy" @click="openEditKey(p.name)">

+              <button class="w-action" :disabled="busy" @click="openEditKey(p)">

-                @click="askDelete(p.name)"


{{ $t("pools.reloginTitle", { name: reloginPool }) }}

{{ $t("pools.reloginTitle", { name: reloginPool.name }) }}


<template>

    <button class="primary" :disabled="busy" @click="openNew('oauth')">
      {{ $t("pools.newOauth") }}
    </button>
    <button class="primary" :disabled="busy" @click="openNew('apikey')">
      {{ $t("pools.newApikey") }}
    </button>

{{ error }}

    <colgroup>
      <col />
      <col class="col-type" />
      <col class="col-projects" />
      <col class="col-actions-wide" />
    </colgroup>

        {{ $t("pools.pool") }}
        {{ $t("pools.type") }}

          {{ p.name }}

          {{ p.credentialType }}

            {{ $t("pools.assigned", p.projects.length) }}

                {{ name }}

            <button :disabled="busy" @click="openRename(p)">
              {{ $t("pools.rename") }}
            </button>
            <template v-if="p.credentialType === 'oauth'">
              <button class="w-action" :disabled="busy" @click="askRelogin(p)">
                {{ $t("pools.relogin") }}
              </button>
            </template>
            <template v-else>
              <button class="w-action" :disabled="busy" @click="openEditKey(p)">
                {{ p.hasCredentials ? $t("pools.changeKey") : $t("pools.insertKey") }}
              </button>
            </template>

              <button
                class="danger"
                :disabled="busy || p.running.length > 0"
                @click="askDelete(p)"
              >
                {{ $t("pools.delete") }}
              </button>

                  {{ $t("pools.deleteBlockedTooltip", { projects: p.running.join(", ") }) }}

{{ $t("pools.empty") }}

    <form class="dialog" @submit.prevent="submit">

{{ dialogTitle }}

        {{ $t("pools.name") }}
        <input v-model="dName" placeholder="z. B. privateDefault" autofocus required />

        {{ $t("pools.apiKey") }}
        <input
          v-model="dKey"
          type="password"
          placeholder="sk-ant-api03-…"
          :autofocus="dialog.editing"
          required
        />

        {{ $t("pools.oauthHint") }}

        <button type="button" :disabled="busy" @click="closeDialog">
          {{ $t("pools.cancel") }}
        </button>
        <button type="submit" class="primary" :disabled="busy">
          {{ dialog.mode === "oauth" ? $t("pools.createPool") : $t("pools.save") }}
        </button>

    </form>

{{ $t("pools.reloginTitle", { name: reloginPool }) }}

      <template v-if="reloginRunning.length">

{{ $t("pools.reloginBlocked") }}

{{ name }}

          <button type="button" @click="reloginPool = null">
            {{ $t("pools.cancel") }}
          </button>

      </template>
      <template v-else>

          {{
            reloginHasEntry
              ? $t("pools.reloginWarning", { name: reloginPool })
              : $t("pools.reloginNoEntry")
          }}

          <button type="button" :disabled="busy" @click="reloginPool = null">
            {{ $t("pools.cancel") }}
          </button>
          <button class="primary" :disabled="busy" @click="confirmRelogin">
            {{ $t("pools.reset") }}
          </button>

      </template>

{{ $t("pools.deletePool") }}

{{ $t("pools.deleteWarning", { name: deleteName }) }}

      <template v-if="deleteProjects.length">

{{ $t("pools.deleteUnassigns") }}

{{ name }}

      </template>

        <button type="button" :disabled="busy" @click="deleteName = null">
          {{ $t("pools.cancel") }}
        </button>
        <button class="danger" :disabled="busy" @click="confirmDelete">
          {{ $t("pools.delete") }}
        </button>

</template>

@click.self="busy ? undefined : (reloginPool = null)"

-      <h3>{{ $t("pools.reloginTitle", { name: reloginPool }) }}</h3>

+      <h3>{{ $t("pools.reloginTitle", { name: reloginPool.name }) }}</h3>


          {{
            reloginHasEntry
              ? $t("pools.reloginWarning", { name: reloginPool })
              : $t("pools.reloginNoEntry")
          }}

          {{
            reloginHasEntry
              ? $t("pools.reloginWarning", { name: reloginPool.name })
              : $t("pools.reloginNoEntry")
          }}


          {{
            reloginHasEntry
              ? $t("pools.reloginWarning", { name: reloginPool.name })
              : $t("pools.reloginNoEntry")
          }}

<template>

    <button class="primary" :disabled="busy" @click="openNew('oauth')">
      {{ $t("pools.newOauth") }}
    </button>
    <button class="primary" :disabled="busy" @click="openNew('apikey')">
      {{ $t("pools.newApikey") }}
    </button>

{{ error }}

    <colgroup>
      <col />
      <col class="col-type" />
      <col class="col-projects" />
      <col class="col-actions-wide" />
    </colgroup>

        {{ $t("pools.pool") }}
        {{ $t("pools.type") }}

          {{ p.name }}

          {{ p.credentialType }}

            {{ $t("pools.assigned", p.projects.length) }}

                {{ name }}

            <button :disabled="busy" @click="openRename(p)">
              {{ $t("pools.rename") }}
            </button>
            <template v-if="p.credentialType === 'oauth'">
              <button class="w-action" :disabled="busy" @click="askRelogin(p)">
                {{ $t("pools.relogin") }}
              </button>
            </template>
            <template v-else>
              <button class="w-action" :disabled="busy" @click="openEditKey(p)">
                {{ p.hasCredentials ? $t("pools.changeKey") : $t("pools.insertKey") }}
              </button>
            </template>

              <button
                class="danger"
                :disabled="busy || p.running.length > 0"
                @click="askDelete(p)"
              >
                {{ $t("pools.delete") }}
              </button>

                  {{ $t("pools.deleteBlockedTooltip", { projects: p.running.join(", ") }) }}

{{ $t("pools.empty") }}

    <form class="dialog" @submit.prevent="submit">

{{ dialogTitle }}

        {{ $t("pools.name") }}
        <input v-model="dName" placeholder="z. B. privateDefault" autofocus required />

        {{ $t("pools.apiKey") }}
        <input
          v-model="dKey"
          type="password"
          placeholder="sk-ant-api03-…"
          :autofocus="dialog.editing"
          required
        />

        {{ $t("pools.oauthHint") }}

        <button type="button" :disabled="busy" @click="closeDialog">
          {{ $t("pools.cancel") }}
        </button>
        <button type="submit" class="primary" :disabled="busy">
          {{ dialog.mode === "oauth" ? $t("pools.createPool") : $t("pools.save") }}
        </button>

    </form>

{{ $t("pools.reloginTitle", { name: reloginPool.name }) }}

      <template v-if="reloginRunning.length">

{{ $t("pools.reloginBlocked") }}

{{ name }}

          <button type="button" @click="reloginPool = null">
            {{ $t("pools.cancel") }}
          </button>

      </template>
      <template v-else>

          {{
            reloginHasEntry
              ? $t("pools.reloginWarning", { name: reloginPool })
              : $t("pools.reloginNoEntry")
          }}

          <button type="button" :disabled="busy" @click="reloginPool = null">
            {{ $t("pools.cancel") }}
          </button>
          <button class="primary" :disabled="busy" @click="confirmRelogin">
            {{ $t("pools.reset") }}
          </button>

      </template>

{{ $t("pools.deletePool") }}

{{ $t("pools.deleteWarning", { name: deleteName }) }}

      <template v-if="deleteProjects.length">

{{ $t("pools.deleteUnassigns") }}

{{ name }}

      </template>

        <button type="button" :disabled="busy" @click="deleteName = null">
          {{ $t("pools.cancel") }}
        </button>
        <button class="danger" :disabled="busy" @click="confirmDelete">
          {{ $t("pools.delete") }}
        </button>

</template>

-              ? $t("pools.reloginWarning", { name: reloginPool })

+              ? $t("pools.reloginWarning", { name: reloginPool.name })


{{ $t("pools.deletePool") }}

{{ $t("pools.deleteWarning", { name: deleteName }) }}

      <template v-if="deleteProjects.length">

{{ $t("pools.deleteUnassigns") }}

{{ name }}

      </template>

        <button type="button" :disabled="busy" @click="deleteName = null">
          {{ $t("pools.cancel") }}
        </button>
        <button class="danger" :disabled="busy" @click="confirmDelete">
          {{ $t("pools.delete") }}
        </button>

</template>

{{ $t("pools.deletePool") }}

{{ $t("pools.deleteWarning", { name: deletePool.name }) }}

      <template v-if="deletePool.projects.length">

{{ $t("pools.deleteUnassigns") }}

{{ name }}

      </template>

        <button type="button" :disabled="busy" @click="deletePool = null">
          {{ $t("pools.cancel") }}
        </button>
        <button class="danger" :disabled="busy" @click="confirmDelete">
          {{ $t("pools.delete") }}
        </button>

    <form class="dialog" @submit.prevent="confirmRename">

{{ $t("pools.renameTitle", { name: renamePool.name }) }}

        {{ $t("pools.name") }}
        <input v-model="rName" autofocus required />

        <button type="button" :disabled="busy" @click="renamePool = null">
          {{ $t("pools.cancel") }}
        </button>
        <button type="submit" class="primary" :disabled="busy">
          {{ $t("pools.save") }}
        </button>

    </form>

</template>


{{ $t("pools.deletePool") }}

{{ $t("pools.deleteWarning", { name: deletePool.name }) }}

      <template v-if="deletePool.projects.length">

{{ $t("pools.deleteUnassigns") }}

{{ name }}

      </template>

        <button type="button" :disabled="busy" @click="deletePool = null">
          {{ $t("pools.cancel") }}
        </button>
        <button class="danger" :disabled="busy" @click="confirmDelete">
          {{ $t("pools.delete") }}
        </button>

    <form class="dialog" @submit.prevent="confirmRename">

{{ $t("pools.renameTitle", { name: renamePool.name }) }}

        {{ $t("pools.name") }}
        <input v-model="rName" autofocus required />

        <button type="button" :disabled="busy" @click="renamePool = null">
          {{ $t("pools.cancel") }}
        </button>
        <button type="submit" class="primary" :disabled="busy">
          {{ $t("pools.save") }}
        </button>

    </form>

</template>

<template>

    <button class="primary" :disabled="busy" @click="openNew('oauth')">
      {{ $t("pools.newOauth") }}
    </button>
    <button class="primary" :disabled="busy" @click="openNew('apikey')">
      {{ $t("pools.newApikey") }}
    </button>

{{ error }}

    <colgroup>
      <col />
      <col class="col-type" />
      <col class="col-projects" />
      <col class="col-actions-wide" />
    </colgroup>

        {{ $t("pools.pool") }}
        {{ $t("pools.type") }}

          {{ p.name }}

          {{ p.credentialType }}

            {{ $t("pools.assigned", p.projects.length) }}

                {{ name }}

            <button :disabled="busy" @click="openRename(p)">
              {{ $t("pools.rename") }}
            </button>
            <template v-if="p.credentialType === 'oauth'">
              <button class="w-action" :disabled="busy" @click="askRelogin(p)">
                {{ $t("pools.relogin") }}
              </button>
            </template>
            <template v-else>
              <button class="w-action" :disabled="busy" @click="openEditKey(p)">
                {{ p.hasCredentials ? $t("pools.changeKey") : $t("pools.insertKey") }}
              </button>
            </template>

              <button
                class="danger"
                :disabled="busy || p.running.length > 0"
                @click="askDelete(p)"
              >
                {{ $t("pools.delete") }}
              </button>

                  {{ $t("pools.deleteBlockedTooltip", { projects: p.running.join(", ") }) }}

{{ $t("pools.empty") }}

    <form class="dialog" @submit.prevent="submit">

{{ dialogTitle }}

        {{ $t("pools.name") }}
        <input v-model="dName" placeholder="z. B. privateDefault" autofocus required />

        {{ $t("pools.apiKey") }}
        <input
          v-model="dKey"
          type="password"
          placeholder="sk-ant-api03-…"
          :autofocus="dialog.editing"
          required
        />

        {{ $t("pools.oauthHint") }}

        <button type="button" :disabled="busy" @click="closeDialog">
          {{ $t("pools.cancel") }}
        </button>
        <button type="submit" class="primary" :disabled="busy">
          {{ dialog.mode === "oauth" ? $t("pools.createPool") : $t("pools.save") }}
        </button>

    </form>

{{ $t("pools.reloginTitle", { name: reloginPool.name }) }}

      <template v-if="reloginRunning.length">

{{ $t("pools.reloginBlocked") }}

{{ name }}

          <button type="button" @click="reloginPool = null">
            {{ $t("pools.cancel") }}
          </button>

      </template>
      <template v-else>

          {{
            reloginHasEntry
              ? $t("pools.reloginWarning", { name: reloginPool.name })
              : $t("pools.reloginNoEntry")
          }}

          <button type="button" :disabled="busy" @click="reloginPool = null">
            {{ $t("pools.cancel") }}
          </button>
          <button class="primary" :disabled="busy" @click="confirmRelogin">
            {{ $t("pools.reset") }}
          </button>

      </template>

{{ $t("pools.deletePool") }}

{{ $t("pools.deleteWarning", { name: deleteName }) }}

      <template v-if="deleteProjects.length">

{{ $t("pools.deleteUnassigns") }}

{{ name }}

      </template>

        <button type="button" :disabled="busy" @click="deleteName = null">
          {{ $t("pools.cancel") }}
        </button>
        <button class="danger" :disabled="busy" @click="confirmDelete">
          {{ $t("pools.delete") }}
        </button>

</template>

-  <div v-if="deleteName" class="overlay" @click.self="deleteName = null">

+  <div v-if="deletePool" class="overlay" @click.self="deletePool = null">

-
{{ $t("pools.deleteWarning", { name: deleteName }) }}

-      <template v-if="deleteProjects.length">

+
{{ $t("pools.deleteWarning", { name: deletePool.name }) }}

+      <template v-if="deletePool.projects.length">

-        <button type="button" :disabled="busy" @click="deleteName = null">

+        <button type="button" :disabled="busy" @click="deletePool = null">

+  <div v-if="renamePool" class="overlay" @click.self="busy ? undefined : (renamePool = null)">

+    <form class="dialog" @submit.prevent="confirmRename">

+      <h3>{{ $t("pools.renameTitle", { name: renamePool.name }) }}</h3>

+        <input v-model="rName" autofocus required />

+        <button type="button" :disabled="busy" @click="renamePool = null">

+        <button type="submit" class="primary" :disabled="busy">


Bash

grep -n "relogin\|deleteWarning\|newOauthPool\|editKey" /Users/marcus.hinz/projects/ai-control/src/i18n.ts | head

i18n-Struktur der pools-Keys ansehen


66:    deleteWarning:
79:    relogin: "Zurücksetzen",
85:    newOauthPool: "Neuen oAuth-Pool anlegen",
87:    editKey: "API-Key ändern – {name}",
97:    deleteWarning:
102:    reloginTitle: "{name} zurücksetzen",
103:    reloginWarning:
105:    reloginNoEntry:
107:    reloginBlocked:
175:    deleteWarning:


55	    name: "Name",
56	    workDir: "Arbeitsordner",
57	    workDirNone: "keiner",
58	    workDirCustom: "vorhandenen Ordner verknüpfen",
59	    chooseFolder: "Ordner wählen …",
60	    create: "Anlegen",
61	    wizardHint:
62	      "Angelegt werden Projektordner mit memory/, Sentinel-.gitignore, settings.json (Memory-Verzeichnis + Berechtigungen) und die Pool-/Terminal-Zuordnung.",
63	    delete: "Projekt löschen",
64	    deleteRunning: "Läuft noch — erst beenden",
65	    deleteTitle: "{name} löschen",
66	    deleteWarning:
67	      "Der Projektordner unter ~/claude-projects (inkl. memory/, Settings und Pool-Zuordnung) wird endgültig gelöscht.",
68	    deleteWorkDirs: "Arbeitsordner mitlöschen:",
69	    workDirsStay: "Die Arbeitsordner bleiben unangetastet.",
70	    todo: "Todoliste",
71	    todoDesc: "OFFENE-PUNKTE.md bei jedem Sessionstart einspielen",
72	  },
73	  pools: {
74	    pool: "Pool",
75	    type: "Typ",
76	    actions: "Aktionen",
77	    newOauth: "+ oAuth",
78	    newApikey: "+ apiKey",
79	    relogin: "Zurücksetzen",
80	    changeKey: "Key ändern",
81	    insertKey: "Key eintragen",
82	    delete: "Löschen",
83	    assigned: "kein Projekt | 1 Projekt | {count} Projekte",
84	    empty: "Noch keine Pools angelegt.",
85	    newOauthPool: "Neuen oAuth-Pool anlegen",
86	    newApikeyPool: "Neuen API-Key-Pool anlegen",
87	    editKey: "API-Key ändern – {name}",
88	    name: "Name",
89	    apiKey: "API-Key",
90	    oauthHint:
91	      "Der Pool wird angelegt; die Anmeldung macht claude beim ersten Start selbst per /login (Browser gegen dein Abo).",
92	    cancel: "Abbrechen",
93	    createPool: "Anlegen",
94	    reset: "Zurücksetzen",
95	    save: "Speichern",
96	    deletePool: "Pool löschen",
97	    deleteWarning:
98	      "Pool {name} und seine Credential-Datei werden gelöscht. Das lässt sich nicht rückgängig machen.",
99	    deleteUnassigns: "Diese Projekte verlieren ihre Pool-Zuordnung:",
100	    deleteBlockedTooltip:
101	      "Löschen gesperrt — {projects} läuft gerade. Erst beenden.",
102	    reloginTitle: "{name} zurücksetzen",
103	    reloginWarning:
104	      "Der im Schlüsselbund gespeicherte Zugriffstoken von {name} wird gelöscht. Beim nächsten Start verlangt claude erneut /login.",
105	    reloginNoEntry:
106	      "Kein Schlüsselbund-Eintrag vorhanden — beim nächsten Start meldet sich claude ohnehin per /login an.",
107	    reloginBlocked:
108	      "Zurücksetzen ist nur bei ungenutztem Pool möglich. Diese Projekte laufen noch:",
109	  },
110	};
111
112	const en: typeof de = {
113	  app: {
114	    projects: "Projects",
115	    pools: "Pools",
116	    usage: "Usage",
117	    autostart: "Autostart",
118	  },
119	  usage: {
120	    days7: "7 days",
121	    days30: "30 days",
122	    pool: "Pool",
123	    project: "Project",
124	    input: "Input",

    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 (Memory-Verzeichnis + Berechtigungen) 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.",
    todo: "Todoliste",
    todoDesc: "OFFENE-PUNKTE.md bei jedem Sessionstart einspielen",
  },
  pools: {
    pool: "Pool",
    type: "Typ",
    actions: "Aktionen",
    newOauth: "+ oAuth",
    newApikey: "+ apiKey",
    relogin: "Zurücksetzen",
    changeKey: "Key ändern",
    insertKey: "Key eintragen",
    delete: "Löschen",
    assigned: "kein Projekt | 1 Projekt | {count} Projekte",
    empty: "Noch keine Pools angelegt.",
    newOauthPool: "Neuen oAuth-Pool anlegen",
    newApikeyPool: "Neuen API-Key-Pool anlegen",
    editKey: "API-Key ändern – {name}",
    name: "Name",
    apiKey: "API-Key",
    oauthHint:
      "Der Pool wird angelegt; die Anmeldung macht claude beim ersten Start selbst per /login (Browser gegen dein Abo).",
    cancel: "Abbrechen",
    createPool: "Anlegen",
    reset: "Zurücksetzen",
    save: "Speichern",
    deletePool: "Pool löschen",
    deleteWarning:
      "Pool {name} und seine Credential-Datei werden gelöscht. Das lässt sich nicht rückgängig machen.",
    deleteUnassigns: "Diese Projekte verlieren ihre Pool-Zuordnung:",
    deleteBlockedTooltip:
      "Löschen gesperrt — {projects} läuft gerade. Erst beenden.",
    reloginTitle: "{name} zurücksetzen",
    reloginWarning:
      "Der im Schlüsselbund gespeicherte Zugriffstoken von {name} wird gelöscht. Beim nächsten Start verlangt claude erneut /login.",
    reloginNoEntry:
      "Kein Schlüsselbund-Eintrag vorhanden — beim nächsten Start meldet sich claude ohnehin per /login an.",
    reloginBlocked:
      "Zurücksetzen ist nur bei ungenutztem Pool möglich. Diese Projekte laufen noch:",
  },
};

const en: typeof de = {
  app: {
    projects: "Projects",
    pools: "Pools",
    usage: "Usage",
    autostart: "Autostart",
  },
  usage: {
    days7: "7 days",
    days30: "30 days",
    pool: "Pool",
    project: "Project",
    input: "Input",


Edit

    relogin: "Zurücksetzen",
    changeKey: "Key ändern",

    relogin: "Zurücksetzen",
    rename: "Umbenennen",
    renameTitle: "{name} umbenennen",
    changeKey: "Key ändern",


The file /Users/marcus.hinz/projects/ai-control/src/i18n.ts has been updated successfully. (file state is current in your context — no need to Read it back)

    relogin: "Zurücksetzen",
    rename: "Umbenennen",
    renameTitle: "{name} umbenennen",
    changeKey: "Key ändern",

import { createI18n } from "vue-i18n";

const de = {
  app: {
    projects: "Projekte",
    pools: "Pools",
    usage: "Verbrauch",
    autostart: "Autostart",
  },
  usage: {
    days7: "7 Tage",
    days30: "30 Tage",
    pool: "Pool",
    project: "Projekt",
    input: "Input",
    output: "Output",
    cacheWrite: "Cache ↑",
    cacheRead: "Cache ↓",
    cost: "≈ Kosten",
    total: "Summe",
    empty: "Keine Verbrauchsdaten im Zeitraum.",
    estimateNote:
      "API-Gegenwert aus den lokalen Transcripts — keine Abrechnung, Historie nur solange claude sie vorhält.",
  },
  projects: {
    project: "Projekt",
    pool: "Pool",
    noPool: "– kein Pool –",
    start: "Starten",
    stop: "Beenden",
    terminalSettings: "Terminal-Einstellungen",
    stillRunning: "{name} läuft noch",
    poolChangeSaved:
      "Der Pool-Wechsel ist gespeichert. Die laufende Session arbeitet aber weiter im alten Pool — der neue gilt erst ab dem nächsten Start.",
    restartHint:
      "Der Neustart beendet die laufende Session. Der automatische commit+push von claude-sync nach Sessionende entfällt dabei.",
    keepRunning: "Weiterlaufen lassen",
    restartNow: "Jetzt neu starten",
    restarting: "Starte neu …",
    terminal: "Terminal — {name}",
    title: "Titel",
    theme: "Theme",
    dockIcon: "Dock-Icon",
    chooseFile: "Datei wählen …",
    remove: "Entfernen",
    defaultIcon: "Standard-Icon",
    appliesNextStart: "Gilt ab dem nächsten Terminal-Start.",
    cancel: "Abbrechen",
    save: "Speichern",
    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 (Memory-Verzeichnis + Berechtigungen) 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.",
    todo: "Todoliste",
    todoDesc: "OFFENE-PUNKTE.md bei jedem Sessionstart einspielen",
  },
  pools: {
    pool: "Pool",
    type: "Typ",
    actions: "Aktionen",
    newOauth: "+ oAuth",
    newApikey: "+ apiKey",
    relogin: "Zurücksetzen",
    changeKey: "Key ändern",
    insertKey: "Key eintragen",
    delete: "Löschen",
    assigned: "kein Projekt | 1 Projekt | {count} Projekte",
    empty: "Noch keine Pools angelegt.",
    newOauthPool: "Neuen oAuth-Pool anlegen",
    newApikeyPool: "Neuen API-Key-Pool anlegen",
    editKey: "API-Key ändern – {name}",
    name: "Name",
    apiKey: "API-Key",
    oauthHint:
      "Der Pool wird angelegt; die Anmeldung macht claude beim ersten Start selbst per /login (Browser gegen dein Abo).",
    cancel: "Abbrechen",
    createPool: "Anlegen",
    reset: "Zurücksetzen",
    save: "Speichern",
    deletePool: "Pool löschen",
    deleteWarning:
      "Pool {name} und seine Credential-Datei werden gelöscht. Das lässt sich nicht rückgängig machen.",
    deleteUnassigns: "Diese Projekte verlieren ihre Pool-Zuordnung:",
    deleteBlockedTooltip:
      "Löschen gesperrt — {projects} läuft gerade. Erst beenden.",
    reloginTitle: "{name} zurücksetzen",
    reloginWarning:
      "Der im Schlüsselbund gespeicherte Zugriffstoken von {name} wird gelöscht. Beim nächsten Start verlangt claude erneut /login.",
    reloginNoEntry:
      "Kein Schlüsselbund-Eintrag vorhanden — beim nächsten Start meldet sich claude ohnehin per /login an.",
    reloginBlocked:
      "Zurücksetzen ist nur bei ungenutztem Pool möglich. Diese Projekte laufen noch:",
  },
};

const en: typeof de = {
  app: {
    projects: "Projects",
    pools: "Pools",
    usage: "Usage",
    autostart: "Autostart",
  },
  usage: {
    days7: "7 days",
    days30: "30 days",
    pool: "Pool",
    project: "Project",
    input: "Input",
    output: "Output",
    cacheWrite: "Cache ↑",
    cacheRead: "Cache ↓",
    cost: "≈ cost",
    total: "Total",
    empty: "No usage data in this period.",
    estimateNote:
      "API equivalent from local transcripts — not a bill; history only as long as claude keeps it.",
  },
  projects: {
    project: "Project",
    pool: "Pool",
    noPool: "– no pool –",
    start: "Start",
    stop: "Stop",
    terminalSettings: "Terminal settings",
    stillRunning: "{name} is still running",
    poolChangeSaved:
      "The pool change is saved. The running session keeps working in the old pool — the new one applies from the next start.",
    restartHint:
      "Restarting ends the running session. The automatic commit+push by claude-sync after the session is skipped.",
    keepRunning: "Keep running",
    restartNow: "Restart now",
    restarting: "Restarting …",
    terminal: "Terminal — {name}",
    title: "Title",
    theme: "Theme",
    dockIcon: "Dock icon",
    chooseFile: "Choose file …",
    remove: "Remove",
    defaultIcon: "Default icon",
    appliesNextStart: "Applies from the next terminal start.",
    cancel: "Cancel",
    save: "Save",
    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 (memory dir + permissions), 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.",
    todo: "Todo list",
    todoDesc: "Inject OFFENE-PUNKTE.md at every session start",
  },
  pools: {
    pool: "Pool",
    type: "Type",
    actions: "Actions",
    newOauth: "+ oAuth",
    newApikey: "+ apiKey",
    relogin: "Reset",
    changeKey: "Change key",
    insertKey: "Insert key",
    delete: "Delete",
    assigned: "no projects | 1 project | {count} projects",
    empty: "No pools yet.",
    newOauthPool: "Create oAuth pool",
    newApikeyPool: "Create API key pool",
    editKey: "Change API key – {name}",
    name: "Name",
    apiKey: "API key",
    oauthHint:
      "The pool is created; claude signs in itself on first start via /login (browser against your subscription).",
    cancel: "Cancel",
    createPool: "Create",
    reset: "Reset",
    save: "Save",
    deletePool: "Delete pool",
    deleteWarning:
      "Pool {name} and its credential file will be deleted. This cannot be undone.",
    deleteUnassigns: "These projects lose their pool assignment:",
    deleteBlockedTooltip:
      "Deletion blocked — active: {projects}. Stop the session first.",
    reloginTitle: "Reset {name}",
    reloginWarning:
      "The access token for {name} stored in the keychain will be deleted. On the next start claude asks for /login again.",
    reloginNoEntry:
      "No keychain entry present — claude signs in via /login on the next start anyway.",
    reloginBlocked:
      "Resetting requires an unused pool. These projects are still running:",
  },
};

const stored = localStorage.getItem("lang");
const initial =
  stored === "de" || stored === "en"
    ? stored
    : navigator.language.startsWith("de")
      ? "de"
      : "en";

export const i18n = createI18n({
  legacy: false,
  globalInjection: true,
  locale: initial,
  fallbackLocale: "en",
  messages: { de, en },
});

export function setLocale(lang: "de" | "en") {
  i18n.global.locale.value = lang;
  localStorage.setItem("lang", lang);
}

newOauth: "+ oAuth",
newApikey: "+ apiKey",
relogin: "Zurücksetzen",
+    rename: "Umbenennen",
+    renameTitle: "{name} umbenennen",
changeKey: "Key ändern",
insertKey: "Key eintragen",
delete: "Löschen",


Bash

grep -n "relogin:\|changeKey:" /Users/marcus.hinz/projects/ai-control/src/i18n.ts

Englische pools-Sektion finden


79:    relogin: "Zurücksetzen",
82:    changeKey: "Key ändern",
190:    relogin: "Reset",
191:    changeKey: "Change key",


186	    type: "Type",
187	    actions: "Actions",
188	    newOauth: "+ oAuth",
189	    newApikey: "+ apiKey",
190	    relogin: "Reset",
191	    changeKey: "Change key",
192	    insertKey: "Insert key",
193	    delete: "Delete",
194	    assigned: "no projects | 1 project | {count} projects",
195	    empty: "No pools yet.",
196	    newOauthPool: "Create oAuth pool",
197	    newApikeyPool: "Create API key pool",

    type: "Type",
    actions: "Actions",
    newOauth: "+ oAuth",
    newApikey: "+ apiKey",
    relogin: "Reset",
    changeKey: "Change key",
    insertKey: "Insert key",
    delete: "Delete",
    assigned: "no projects | 1 project | {count} projects",
    empty: "No pools yet.",
    newOauthPool: "Create oAuth pool",
    newApikeyPool: "Create API key pool",


Edit

    relogin: "Reset",
    changeKey: "Change key",

    relogin: "Reset",
    rename: "Rename",
    renameTitle: "Rename {name}",
    changeKey: "Change key",


    relogin: "Reset",
    rename: "Rename",
    renameTitle: "Rename {name}",
    changeKey: "Change key",

import { createI18n } from "vue-i18n";

const de = {
  app: {
    projects: "Projekte",
    pools: "Pools",
    usage: "Verbrauch",
    autostart: "Autostart",
  },
  usage: {
    days7: "7 Tage",
    days30: "30 Tage",
    pool: "Pool",
    project: "Projekt",
    input: "Input",
    output: "Output",
    cacheWrite: "Cache ↑",
    cacheRead: "Cache ↓",
    cost: "≈ Kosten",
    total: "Summe",
    empty: "Keine Verbrauchsdaten im Zeitraum.",
    estimateNote:
      "API-Gegenwert aus den lokalen Transcripts — keine Abrechnung, Historie nur solange claude sie vorhält.",
  },
  projects: {
    project: "Projekt",
    pool: "Pool",
    noPool: "– kein Pool –",
    start: "Starten",
    stop: "Beenden",
    terminalSettings: "Terminal-Einstellungen",
    stillRunning: "{name} läuft noch",
    poolChangeSaved:
      "Der Pool-Wechsel ist gespeichert. Die laufende Session arbeitet aber weiter im alten Pool — der neue gilt erst ab dem nächsten Start.",
    restartHint:
      "Der Neustart beendet die laufende Session. Der automatische commit+push von claude-sync nach Sessionende entfällt dabei.",
    keepRunning: "Weiterlaufen lassen",
    restartNow: "Jetzt neu starten",
    restarting: "Starte neu …",
    terminal: "Terminal — {name}",
    title: "Titel",
    theme: "Theme",
    dockIcon: "Dock-Icon",
    chooseFile: "Datei wählen …",
    remove: "Entfernen",
    defaultIcon: "Standard-Icon",
    appliesNextStart: "Gilt ab dem nächsten Terminal-Start.",
    cancel: "Abbrechen",
    save: "Speichern",
    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 (Memory-Verzeichnis + Berechtigungen) 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.",
    todo: "Todoliste",
    todoDesc: "OFFENE-PUNKTE.md bei jedem Sessionstart einspielen",
  },
  pools: {
    pool: "Pool",
    type: "Typ",
    actions: "Aktionen",
    newOauth: "+ oAuth",
    newApikey: "+ apiKey",
    relogin: "Zurücksetzen",
    rename: "Umbenennen",
    renameTitle: "{name} umbenennen",
    changeKey: "Key ändern",
    insertKey: "Key eintragen",
    delete: "Löschen",
    assigned: "kein Projekt | 1 Projekt | {count} Projekte",
    empty: "Noch keine Pools angelegt.",
    newOauthPool: "Neuen oAuth-Pool anlegen",
    newApikeyPool: "Neuen API-Key-Pool anlegen",
    editKey: "API-Key ändern – {name}",
    name: "Name",
    apiKey: "API-Key",
    oauthHint:
      "Der Pool wird angelegt; die Anmeldung macht claude beim ersten Start selbst per /login (Browser gegen dein Abo).",
    cancel: "Abbrechen",
    createPool: "Anlegen",
    reset: "Zurücksetzen",
    save: "Speichern",
    deletePool: "Pool löschen",
    deleteWarning:
      "Pool {name} und seine Credential-Datei werden gelöscht. Das lässt sich nicht rückgängig machen.",
    deleteUnassigns: "Diese Projekte verlieren ihre Pool-Zuordnung:",
    deleteBlockedTooltip:
      "Löschen gesperrt — {projects} läuft gerade. Erst beenden.",
    reloginTitle: "{name} zurücksetzen",
    reloginWarning:
      "Der im Schlüsselbund gespeicherte Zugriffstoken von {name} wird gelöscht. Beim nächsten Start verlangt claude erneut /login.",
    reloginNoEntry:
      "Kein Schlüsselbund-Eintrag vorhanden — beim nächsten Start meldet sich claude ohnehin per /login an.",
    reloginBlocked:
      "Zurücksetzen ist nur bei ungenutztem Pool möglich. Diese Projekte laufen noch:",
  },
};

const en: typeof de = {
  app: {
    projects: "Projects",
    pools: "Pools",
    usage: "Usage",
    autostart: "Autostart",
  },
  usage: {
    days7: "7 days",
    days30: "30 days",
    pool: "Pool",
    project: "Project",
    input: "Input",
    output: "Output",
    cacheWrite: "Cache ↑",
    cacheRead: "Cache ↓",
    cost: "≈ cost",
    total: "Total",
    empty: "No usage data in this period.",
    estimateNote:
      "API equivalent from local transcripts — not a bill; history only as long as claude keeps it.",
  },
  projects: {
    project: "Project",
    pool: "Pool",
    noPool: "– no pool –",
    start: "Start",
    stop: "Stop",
    terminalSettings: "Terminal settings",
    stillRunning: "{name} is still running",
    poolChangeSaved:
      "The pool change is saved. The running session keeps working in the old pool — the new one applies from the next start.",
    restartHint:
      "Restarting ends the running session. The automatic commit+push by claude-sync after the session is skipped.",
    keepRunning: "Keep running",
    restartNow: "Restart now",
    restarting: "Restarting …",
    terminal: "Terminal — {name}",
    title: "Title",
    theme: "Theme",
    dockIcon: "Dock icon",
    chooseFile: "Choose file …",
    remove: "Remove",
    defaultIcon: "Default icon",
    appliesNextStart: "Applies from the next terminal start.",
    cancel: "Cancel",
    save: "Save",
    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 (memory dir + permissions), 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.",
    todo: "Todo list",
    todoDesc: "Inject OFFENE-PUNKTE.md at every session start",
  },
  pools: {
    pool: "Pool",
    type: "Type",
    actions: "Actions",
    newOauth: "+ oAuth",
    newApikey: "+ apiKey",
    relogin: "Reset",
    changeKey: "Change key",
    insertKey: "Insert key",
    delete: "Delete",
    assigned: "no projects | 1 project | {count} projects",
    empty: "No pools yet.",
    newOauthPool: "Create oAuth pool",
    newApikeyPool: "Create API key pool",
    editKey: "Change API key – {name}",
    name: "Name",
    apiKey: "API key",
    oauthHint:
      "The pool is created; claude signs in itself on first start via /login (browser against your subscription).",
    cancel: "Cancel",
    createPool: "Create",
    reset: "Reset",
    save: "Save",
    deletePool: "Delete pool",
    deleteWarning:
      "Pool {name} and its credential file will be deleted. This cannot be undone.",
    deleteUnassigns: "These projects lose their pool assignment:",
    deleteBlockedTooltip:
      "Deletion blocked — active: {projects}. Stop the session first.",
    reloginTitle: "Reset {name}",
    reloginWarning:
      "The access token for {name} stored in the keychain will be deleted. On the next start claude asks for /login again.",
    reloginNoEntry:
      "No keychain entry present — claude signs in via /login on the next start anyway.",
    reloginBlocked:
      "Resetting requires an unused pool. These projects are still running:",
  },
};

const stored = localStorage.getItem("lang");
const initial =
  stored === "de" || stored === "en"
    ? stored
    : navigator.language.startsWith("de")
      ? "de"
      : "en";

export const i18n = createI18n({
  legacy: false,
  globalInjection: true,
  locale: initial,
  fallbackLocale: "en",
  messages: { de, en },
});

export function setLocale(lang: "de" | "en") {
  i18n.global.locale.value = lang;
  localStorage.setItem("lang", lang);
}

newOauth: "+ oAuth",
newApikey: "+ apiKey",
relogin: "Reset",
+    rename: "Rename",
+    renameTitle: "Rename {name}",
changeKey: "Change key",
insertKey: "Insert key",
delete: "Delete",


<input v-model="dName" placeholder="z. B. privateDefault" autofocus required />

<input v-model="dName" placeholder="z. B. private" autofocus required />


-        <input v-model="dName" placeholder="z. B. privateDefault" autofocus required />

+        <input v-model="dName" placeholder="z. B. private" autofocus required />

<label v-if="dialog.mode === 'apikey'" class="field">


Bash

grep -n '"scripts"' -A8 /Users/marcus.hinz/projects/ai-control/package.json

npm-Scripts ansehen


6:  "scripts": {
7-    "dev": "vite",
8-    "build": "vue-tsc -b && vite build",
9-    "preview": "vite preview",
10-    "tauri": "tauri"
11-  },
12-  "dependencies": {
13-    "@fontsource/jetbrains-mono": "^5.2.8",
14-    "@tauri-apps/api": "^2.11.1",


cd /Users/marcus.hinz/projects/ai-control && zsh -ic 'npm run build' 2>&1 | tail -15

Frontend-Typecheck und Build (vue-tsc + vite)


dist/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff        28.20 kB
dist/assets/jetbrains-mono-latin-700-normal-D3wTyLJW.woff        28.20 kB
dist/assets/main-DyCgaeQx.css                                     8.12 kB │ gzip:   2.30 kB
dist/assets/core-CWMpccrR.css                                    22.48 kB │ gzip:  14.08 kB
dist/assets/terminal-C8JC9f1f.css                                26.37 kB │ gzip:  15.18 kB
dist/assets/core-Bv1HNcCz.js                                      2.27 kB │ gzip:   1.11 kB
dist/assets/main-B38yoxua.js                                    196.52 kB │ gzip:  68.93 kB
dist/assets/terminal-DRQoelm4.js                                678.64 kB │ gzip: 186.29 kB

✓ built in 771ms
[plugin builtin:vite-reporter]
(!) Some chunks are larger than 500 kB after minification. Consider:
- Using dynamic import() to code-split the application
- Use build.rolldownOptions.output.codeSplitting to improve chunking: https://rolldown.rs/reference/OutputOptions.codeSplitting
- Adjust chunk size limit for this warning via build.chunkSizeWarningLimit.


grep -n "p.pool\|\.pool\b" /Users/marcus.hinz/projects/ai-control/src/components/ProjectList.vue | grep -v "wizard.pool\|pool.id\|pool.name"

Restliche Anzeige von Projekt-Pool prüfen


22:  if (!p.pool) return false;
23:  const pool = pools.value.find((x) => x.id === p.pool);
35:  return p.pool ? "projects.needsKey" : "projects.needsPool";
111:  const from = project.pool;
251:      pool: w.pool === "" ? null : w.pool,
330:        <th>{{ $t("projects.pool") }}</th>
347:          <select :value="p.pool ?? ''" @change="assign(p, $event)">
397:        {{ $t("projects.pool") }}


Fertig. Name und Pfad sind jetzt getrennt:

**Rust (`lib.rs`)**
- `PoolInfo` trägt `id` (Ordnername unter `pools/`) und `name` (Anzeigename aus `pool.json`).
- Neuanlage: Ordnername ist eine UUID v4, der eingegebene Name landet nur in `pool.json`. Doppelt-Prüfung läuft über den Anzeigenamen. `create_*_pool` liefert die neue ID zurück.
- Neuer Command `rename_pool(pool, name)`: schreibt nur `pool.json` — ID, Ordner, Keychain-Suffix, Symlinks und Projekt-Zuordnungen bleiben unverändert. Vergebene Namen werden abgewiesen.
- Projekt-`ai-control.json` referenziert die ID; `claude-sync` bleibt unverändert.
- Verbrauch-Tab zeigt den Anzeigenamen (aus `pool.json` aufgelöst, Ordner ohne `pool.json` erscheinen unter der ID).

**UI**
- PoolList: neue Aktion „Umbenennen" mit Dialog; alle Operationen (Key, Relogin, Löschen) laufen über die ID, angezeigt wird der Name.
- ProjectList: Pool-Dropdowns mit ID als Wert und Name als Label; der Restart-Dialog nach Pool-Wechsel zeigt Namen statt IDs.

**Bestand:** `private` und `apiDefault` behalten ihre sprechenden Ordnernamen — dort ist ID = Name, keine Migration nötig.

Verifiziert: 41 Rust-Tests grün (zwei neue: Umbenennen ändert nur `pool.json`, vergebener Name scheitert), `vue-tsc` + vite-Build fehlerfrei. Ein Fix unterwegs: die Namens-Prüfung bei Neuanlage darf beim allerersten Pool nicht an fehlendem `pools/` scheitern.


ok --
gut -- die API Keys - ziehen wir schon aus der KeyChain?


Nein. API-Keys liegen als Datei `apikey` (0600) im Pool-Ordner, `apiKeyHelper` in der Pool-`settings.json` macht `cat` darauf. Im Keychain liegen nur die OAuth-Credentials, die claude selbst dort ablegt.


genau
können wir das für MacOS in die KeyChain verlagern
unter Linux nehmen wir dann keyRing --> im Ordner ablegen als FallBack -> falls KeyChain / keyRing nicht verfügbar
--> wir müssen eh noch schauen, wie wir das unter Windows machen


Plan:

1. **Ablage:** neue Dependency `keyring`-Crate (nativ: macOS Security Framework, Linux Secret Service, später Windows Credential Manager). Eintrag pro Pool: Service `ai-control-apikey`, Account = Pool-ID. `set_apikey`/`create_apikey_pool` schreiben zuerst dorthin; scheitert das (kein Keychain/Keyring verfügbar), Datei wie heute — Fallback nur dann.
2. **Lesen durch claude:** `apiKeyHelper` in der Pool-`settings.json` wird zur Kette:
   - macOS: `security find-generic-password -w -s ai-control-apikey -a <id> 2>/dev/null || cat '<datei>'`
   - Linux: `secret-tool lookup service ai-control-apikey username <id> 2>/dev/null || cat '<datei>'` (Attribute passen zu dem, was die keyring-Crate über Secret Service anlegt)
3. **hasCredentials:** prüft Keychain-Eintrag (nativer API-Call, kein Subprozess) oder Datei.
4. **Löschen/Key ändern:** `delete_pool` räumt den Keychain-Eintrag mit ab; „Key ändern" schreibt in den Keychain und löscht eine vorhandene Key-Datei (= Migration von apiDefault beim nächsten Key-Ändern, kein separater Migrationsschritt).
5. **Windows:** Ablage wäre über die Crate abgedeckt, der apiKeyHelper-Leseweg bleibt offen — wie besprochen später.

Go?


genau können wir das für MacOS in die KeyChain verlagern unter Linux nehmen wir dann keyRing --> im Ordner ablegen als FallBack -> falls KeyChain / keyRing nicht verfügbar --> wir müssen eh noch schau…


ja


810	  }
811	  for project in projects_using_pool(paths, name)? {
812	    unassign_pool_in(paths, &project)?;
813	  }
814	  fs::remove_dir_all(&dir).map_err(|e| e.to_string())
815	}
816
817	/// Schreibt den API-Key eines apikey-Pools neu (0600).
818	fn set_apikey_in(paths: &Paths, pool: &str, key: &str) -> Result<(), String> {
819	  let p = read_pool(paths, pool)?;
820	  if p.credential_type != "apikey" {
821	    return Err(format!("Pool {pool} ist kein apikey-Pool"));
822	  }
823	  let key = key.trim();
824	  if key.is_empty() {
825	    return Err("leerer API-Key".into());
826	  }
827	  write_secret_file(&paths.pool_dir(pool).join(APIKEY_FILE), &format!("{key}\n"))
828	}
829
830	fn ensure_oauth_pool(paths: &Paths, pool: &str) -> Result<(), String> {
831	  let p = read_pool(paths, pool)?;
832	  if p.credential_type != "oauth" {
833	    return Err(format!("Pool {pool} ist kein oauth-Pool"));
834	  }
835	  Ok(())
836	}
837
838	// ---------- Verbrauch ----------
839
840	#[derive(Serialize, Default)]
841	struct UsageTotals {
842	  #[serde(rename = "inputTokens")]
843	  input_tokens: u64,
844	  #[serde(rename = "outputTokens")]
845	  output_tokens: u64,
846	  #[serde(rename = "cacheCreationTokens")]
847	  cache_creation_tokens: u64,
848	  #[serde(rename = "cacheReadTokens")]
849	  cache_read_tokens: u64,
850	  #[serde(rename = "costUsd")]
851	  cost_usd: f64,
852	}
853
854	#[derive(Serialize)]

  }
  for project in projects_using_pool(paths, name)? {
    unassign_pool_in(paths, &project)?;
  }
  fs::remove_dir_all(&dir).map_err(|e| e.to_string())
}

/// Schreibt den API-Key eines apikey-Pools neu (0600).
fn set_apikey_in(paths: &Paths, pool: &str, key: &str) -> Result<(), String> {
  let p = read_pool(paths, pool)?;
  if p.credential_type != "apikey" {
    return Err(format!("Pool {pool} ist kein apikey-Pool"));
  }
  let key = key.trim();
  if key.is_empty() {
    return Err("leerer API-Key".into());
  }
  write_secret_file(&paths.pool_dir(pool).join(APIKEY_FILE), &format!("{key}\n"))
}

fn ensure_oauth_pool(paths: &Paths, pool: &str) -> Result<(), String> {
  let p = read_pool(paths, pool)?;
  if p.credential_type != "oauth" {
    return Err(format!("Pool {pool} ist kein oauth-Pool"));
  }
  Ok(())
}

// ---------- Verbrauch ----------

#[derive(Serialize, Default)]
struct UsageTotals {
  #[serde(rename = "inputTokens")]
  input_tokens: u64,
  #[serde(rename = "outputTokens")]
  output_tokens: u64,
  #[serde(rename = "cacheCreationTokens")]
  cache_creation_tokens: u64,
  #[serde(rename = "cacheReadTokens")]
  cache_read_tokens: u64,
  #[serde(rename = "costUsd")]
  cost_usd: f64,
}

#[derive(Serialize)]


Edit

uuid = { version = "1", features = ["v4"] }

uuid = { version = "1", features = ["v4"] }
keyring = { version = "3", features = ["apple-native", "sync-secret-service", "windows-native"] }


The file /Users/marcus.hinz/projects/ai-control/src-tauri/Cargo.toml has been updated successfully. (file state is current in your context — no need to Read it back)

uuid = { version = "1", features = ["v4"] }
keyring = { version = "3", features = ["apple-native", "sync-secret-service", "windows-native"] }

[package]
name = "ai-control"
version = "0.1.0"
description = "Pool- und Session-Verwaltung für Claude Code"
authors = ["marcus.hinz"]
license = ""
repository = ""
edition = "2021"
rust-version = "1.77.2"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[lib]
name = "app_lib"
crate-type = ["staticlib", "cdylib", "rlib"]

[build-dependencies]
tauri-build = { version = "2.6.3", features = [] }

[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
log = "0.4"
tauri = { version = "2.11.3", features = ["tray-icon", "image-png"] }
tauri-plugin-log = "2"
tauri-plugin-autostart = "2"
tauri-plugin-dialog = "2"
base64 = "0.22"
sha2 = "0.10"
portable-pty = "0.9"
uuid = { version = "1", features = ["v4"] }

[target.'cfg(target_os = "macos")'.dependencies]
objc2 = "0.6"
objc2-app-kit = { version = "0.3", features = ["NSApplication", "NSImage", "NSResponder"] }
objc2-foundation = { version = "0.3", features = ["NSString"] }

+keyring = { version = "3", features = ["apple-native", "sync-secret-service", "windows-native"] }


602	// ---------- Pools ----------
603
604	#[derive(Serialize)]
605	struct PoolInfo {
606	  /// Ordnername unter pools/ (bei Neuanlagen eine UUID) — stabile ID, an der
607	  /// Keychain-Suffix, Symlinks und Projekt-Zuordnungen hängen.
608	  id: String,
609	  /// Anzeigename aus pool.json, frei umbenennbar.
610	  name: String,
611	  #[serde(rename = "credentialType")]
612	  credential_type: String,
613	  projects: Vec<String>,
614	  /// Teilmenge von `projects`, die gerade läuft (dasselbe `is_running` wie die
615	  /// Projektliste). Der Löschen-Dialog sperrt darauf.
616	  running: Vec<String>,
617	  #[serde(rename = "hasCredentials")]
618	  has_credentials: bool,
619	}
620
621	fn list_pools_in(paths: &Paths) -> Result<Vec<PoolInfo>, String> {
622	  let mut pools = Vec::new();
623	  let entries = fs::read_dir(paths.pools_dir()).map_err(|e| e.to_string())?;
624	  for entry in entries {
625	    let entry = entry.map_err(|e| e.to_string())?;
626	    let cfg_path = entry.path().join(POOL_FILE);
627	    if !cfg_path.is_file() {
628	      continue;
629	    }
630	    let raw = fs::read_to_string(&cfg_path).map_err(|e| e.to_string())?;
631	    let pool: Pool =
632	      serde_json::from_str(&raw).map_err(|e| format!("{}: {e}", cfg_path.display()))?;
633	    // oauth: Credentials liegen im Keychain, dessen Prüfung wäre ein
634	    // security-Aufruf pro Pool im 3-s-Polling — deshalb hier immer true.
635	    let has_credentials = match pool.credential_type.as_str() {
636	      "apikey" => fs::read_to_string(entry.path().join(APIKEY_FILE))
637	        .map(|s| !s.trim().is_empty())
638	        .unwrap_or(false),
639	      _ => true,
640	    };
641	    let id = entry.file_name().to_string_lossy().into_owned();
642	    let projects = projects_using_pool(paths, &id)?;
643	    let running = projects.iter().filter(|p| is_running(p)).cloned().collect();
644	    pools.push(PoolInfo {
645	      id,
646	      projects,
647	      running,
648	      name: pool.name,
649	      credential_type: pool.credential_type,
650	      has_credentials,
651	    });
652	  }
653	  pools.sort_by(|a, b| a.name.cmp(&b.name));
654	  Ok(pools)
655	}
656
657	/// Prüft den Anzeigenamen (gültig + noch nicht vergeben) und liefert den
658	/// Ordner für einen neuen Pool: pools/<UUID v4>.
659	fn check_new_pool(paths: &Paths, name: &str) -> Result<PathBuf, String> {
660	  check_name(name)?;
661	  // Beim ersten Pool existiert pools/ noch nicht — dann ist kein Name vergeben.
662	  if paths.pools_dir().is_dir() {
663	    for pool in list_pools_in(paths)? {
664	      if pool.name == name {
665	        return Err(format!("Pool existiert bereits: {name}"));
666	      }
667	    }
668	  }
669	  Ok(paths.pool_dir(&uuid::Uuid::new_v4().to_string()))
670	}
671
672	fn write_pool_json(dir: &PathBuf, name: &str, credential_type: &str) -> Result<(), String> {
673	  fs::create_dir_all(dir).map_err(|e| e.to_string())?;
674	  let pool = Pool {
675	    name: name.to_string(),
676	    credential_type: credential_type.to_string(),
677	  };
678	  let raw = serde_json::to_string_pretty(&pool).map_err(|e| e.to_string())?;
679	  fs::write(dir.join(POOL_FILE), raw + "\n").map_err(|e| e.to_string())
680	}
681

// ---------- Pools ----------

#[derive(Serialize)]
struct PoolInfo {
  /// Ordnername unter pools/ (bei Neuanlagen eine UUID) — stabile ID, an der
  /// Keychain-Suffix, Symlinks und Projekt-Zuordnungen hängen.
  id: String,
  /// Anzeigename aus pool.json, frei umbenennbar.
  name: String,
  #[serde(rename = "credentialType")]
  credential_type: String,
  projects: Vec<String>,
  /// Teilmenge von `projects`, die gerade läuft (dasselbe `is_running` wie die
  /// Projektliste). Der Löschen-Dialog sperrt darauf.
  running: Vec<String>,
  #[serde(rename = "hasCredentials")]
  has_credentials: bool,
}

fn list_pools_in(paths: &Paths) -> Result<Vec<PoolInfo>, String> {
  let mut pools = Vec::new();
  let entries = fs::read_dir(paths.pools_dir()).map_err(|e| e.to_string())?;
  for entry in entries {
    let entry = entry.map_err(|e| e.to_string())?;
    let cfg_path = entry.path().join(POOL_FILE);
    if !cfg_path.is_file() {
      continue;
    }
    let raw = fs::read_to_string(&cfg_path).map_err(|e| e.to_string())?;
    let pool: Pool =
      serde_json::from_str(&raw).map_err(|e| format!("{}: {e}", cfg_path.display()))?;
    // oauth: Credentials liegen im Keychain, dessen Prüfung wäre ein
    // security-Aufruf pro Pool im 3-s-Polling — deshalb hier immer true.
    let has_credentials = match pool.credential_type.as_str() {
      "apikey" => fs::read_to_string(entry.path().join(APIKEY_FILE))
        .map(|s| !s.trim().is_empty())
        .unwrap_or(false),
      _ => true,
    };
    let id = entry.file_name().to_string_lossy().into_owned();
    let projects = projects_using_pool(paths, &id)?;
    let running = projects.iter().filter(|p| is_running(p)).cloned().collect();
    pools.push(PoolInfo {
      id,
      projects,
      running,
      name: pool.name,
      credential_type: pool.credential_type,
      has_credentials,
    });
  }
  pools.sort_by(|a, b| a.name.cmp(&b.name));
  Ok(pools)
}

/// Prüft den Anzeigenamen (gültig + noch nicht vergeben) und liefert den
/// Ordner für einen neuen Pool: pools/<UUID v4>.
fn check_new_pool(paths: &Paths, name: &str) -> Result<PathBuf, String> {
  check_name(name)?;
  // Beim ersten Pool existiert pools/ noch nicht — dann ist kein Name vergeben.
  if paths.pools_dir().is_dir() {
    for pool in list_pools_in(paths)? {
      if pool.name == name {
        return Err(format!("Pool existiert bereits: {name}"));
      }
    }
  }
  Ok(paths.pool_dir(&uuid::Uuid::new_v4().to_string()))
}

fn write_pool_json(dir: &PathBuf, name: &str, credential_type: &str) -> Result<(), String> {
  fs::create_dir_all(dir).map_err(|e| e.to_string())?;
  let pool = Pool {
    name: name.to_string(),
    credential_type: credential_type.to_string(),
  };
  let raw = serde_json::to_string_pretty(&pool).map_err(|e| e.to_string())?;
  fs::write(dir.join(POOL_FILE), raw + "\n").map_err(|e| e.to_string())
}


// ---------- Pools ----------

#[derive(Serialize)]
struct PoolInfo {

// ---------- Pools ----------

/// Ablage der API-Keys: macOS-Keychain / Linux-Secret-Service (keyring-Crate).
/// Die Key-Datei im Pool-Ordner bleibt Fallback, wenn der Store beim Schreiben
/// nicht verfügbar ist.
trait ApikeyStore {
  fn set(&self, pool: &str, key: &str) -> Result<(), String>;
  fn has(&self, pool: &str) -> Result<bool, String>;
  fn delete(&self, pool: &str) -> Result<(), String>;
}

/// Service-Name der Einträge; Account ist die Pool-ID. Unter Linux legt die
/// keyring-Crate die Attribute service/username an — der apiKeyHelper liest
/// mit denselben Attributen über secret-tool.
const APIKEY_SERVICE: &str = "ai-control-apikey";

struct KeychainStore;

impl KeychainStore {
  fn entry(pool: &str) -> Result<keyring::Entry, String> {
    keyring::Entry::new(APIKEY_SERVICE, pool).map_err(|e| e.to_string())
  }
}

impl ApikeyStore for KeychainStore {
  fn set(&self, pool: &str, key: &str) -> Result<(), String> {
    Self::entry(pool)?.set_password(key).map_err(|e| e.to_string())
  }

  fn has(&self, pool: &str) -> Result<bool, String> {
    match Self::entry(pool)?.get_password() {
      Ok(_) => Ok(true),
      // Kein Eintrag oder kein Store verfügbar → die Key-Datei entscheidet.
      Err(
        keyring::Error::NoEntry
        | keyring::Error::PlatformFailure(_)
        | keyring::Error::NoStorageAccess(_),
      ) => Ok(false),
      Err(e) => Err(e.to_string()),
    }
  }

  fn delete(&self, pool: &str) -> Result<(), String> {
    match Self::entry(pool)?.delete_credential() {
      Ok(())
      | Err(
        keyring::Error::NoEntry
        | keyring::Error::PlatformFailure(_)
        | keyring::Error::NoStorageAccess(_),
      ) => Ok(()),
      Err(e) => Err(e.to_string()),
    }
  }
}

/// apiKeyHelper-Kommando eines apikey-Pools: liest den Key aus dem
/// Keychain/Keyring, bei fehlendem Eintrag aus der Key-Datei.
fn apikey_helper_command(dir: &std::path::Path, pool_id: &str) -> String {
  let file = dir.join(APIKEY_FILE);
  if cfg!(target_os = "macos") {
    format!(
      "security find-generic-password -w -s {APIKEY_SERVICE} -a {pool_id} 2>/dev/null || cat '{}'",
      file.display()
    )
  } else {
    format!(
      "secret-tool lookup service {APIKEY_SERVICE} username {pool_id} 2>/dev/null || cat '{}'",
      file.display()
    )
  }
}

#[derive(Serialize)]
struct PoolInfo {


The file /Users/marcus.hinz/projects/ai-control/src-tauri/src/lib.rs has been updated successfully. (file state is current in your context — no need to Read it back)

// ---------- Pools ----------

#[derive(Serialize)]
struct PoolInfo {

// ---------- Pools ----------

/// Ablage der API-Keys: macOS-Keychain / Linux-Secret-Service (keyring-Crate).
/// Die Key-Datei im Pool-Ordner bleibt Fallback, wenn der Store beim Schreiben
/// nicht verfügbar ist.
trait ApikeyStore {
  fn set(&self, pool: &str, key: &str) -> Result<(), String>;
  fn has(&self, pool: &str) -> Result<bool, String>;
  fn delete(&self, pool: &str) -> Result<(), String>;
}

/// Service-Name der Einträge; Account ist die Pool-ID. Unter Linux legt die
/// keyring-Crate die Attribute service/username an — der apiKeyHelper liest
/// mit denselben Attributen über secret-tool.
const APIKEY_SERVICE: &str = "ai-control-apikey";

struct KeychainStore;

impl KeychainStore {
  fn entry(pool: &str) -> Result<keyring::Entry, String> {
    keyring::Entry::new(APIKEY_SERVICE, pool).map_err(|e| e.to_string())
  }
}

impl ApikeyStore for KeychainStore {
  fn set(&self, pool: &str, key: &str) -> Result<(), String> {
    Self::entry(pool)?.set_password(key).map_err(|e| e.to_string())
  }

  fn has(&self, pool: &str) -> Result<bool, String> {
    match Self::entry(pool)?.get_password() {
      Ok(_) => Ok(true),
      // Kein Eintrag oder kein Store verfügbar → die Key-Datei entscheidet.
      Err(
        keyring::Error::NoEntry
        | keyring::Error::PlatformFailure(_)
        | keyring::Error::NoStorageAccess(_),
      ) => Ok(false),
      Err(e) => Err(e.to_string()),
    }
  }

  fn delete(&self, pool: &str) -> Result<(), String> {
    match Self::entry(pool)?.delete_credential() {
      Ok(())
      | Err(
        keyring::Error::NoEntry
        | keyring::Error::PlatformFailure(_)
        | keyring::Error::NoStorageAccess(_),
      ) => Ok(()),
      Err(e) => Err(e.to_string()),
    }
  }
}

/// apiKeyHelper-Kommando eines apikey-Pools: liest den Key aus dem
/// Keychain/Keyring, bei fehlendem Eintrag aus der Key-Datei.
fn apikey_helper_command(dir: &std::path::Path, pool_id: &str) -> String {
  let file = dir.join(APIKEY_FILE);
  if cfg!(target_os = "macos") {
    format!(
      "security find-generic-password -w -s {APIKEY_SERVICE} -a {pool_id} 2>/dev/null || cat '{}'",
      file.display()
    )
  } else {
    format!(
      "secret-tool lookup service {APIKEY_SERVICE} username {pool_id} 2>/dev/null || cat '{}'",
      file.display()
    )
  }
}

#[derive(Serialize)]
struct PoolInfo {

+/// Ablage der API-Keys: macOS-Keychain / Linux-Secret-Service (keyring-Crate).

+/// Die Key-Datei im Pool-Ordner bleibt Fallback, wenn der Store beim Schreiben

+  fn set(&self, pool: &str, key: &str) -> Result<(), String>;

+  fn has(&self, pool: &str) -> Result<bool, String>;

+  fn delete(&self, pool: &str) -> Result<(), String>;

+/// Service-Name der Einträge; Account ist die Pool-ID. Unter Linux legt die

+/// keyring-Crate die Attribute service/username an — der apiKeyHelper liest

+/// mit denselben Attributen über secret-tool.

+const APIKEY_SERVICE: &str = "ai-control-apikey";

+  fn entry(pool: &str) -> Result<keyring::Entry, String> {

+    keyring::Entry::new(APIKEY_SERVICE, pool).map_err(|e| e.to_string())

+  fn set(&self, pool: &str, key: &str) -> Result<(), String> {

+    Self::entry(pool)?.set_password(key).map_err(|e| e.to_string())

+  fn has(&self, pool: &str) -> Result<bool, String> {

+    match Self::entry(pool)?.get_password() {

+      // Kein Eintrag oder kein Store verfügbar → die Key-Datei entscheidet.

+        | keyring::Error::PlatformFailure(_)

+        | keyring::Error::NoStorageAccess(_),

+  fn delete(&self, pool: &str) -> Result<(), String> {

+    match Self::entry(pool)?.delete_credential() {

+/// apiKeyHelper-Kommando eines apikey-Pools: liest den Key aus dem

+/// Keychain/Keyring, bei fehlendem Eintrag aus der Key-Datei.

+fn apikey_helper_command(dir: &std::path::Path, pool_id: &str) -> String {

+      "security find-generic-password -w -s {APIKEY_SERVICE} -a {pool_id} 2>/dev/null || cat '{}'",

+      "secret-tool lookup service {APIKEY_SERVICE} username {pool_id} 2>/dev/null || cat '{}'",

/// Ordnername unter pools/ (bei Neuanlagen eine UUID) — stabile ID, an der


fn list_pools_in(paths: &Paths) -> Result<Vec<PoolInfo>, String> {
  let mut pools = Vec::new();
  let entries = fs::read_dir(paths.pools_dir()).map_err(|e| e.to_string())?;
  for entry in entries {
    let entry = entry.map_err(|e| e.to_string())?;
    let cfg_path = entry.path().join(POOL_FILE);
    if !cfg_path.is_file() {
      continue;
    }
    let raw = fs::read_to_string(&cfg_path).map_err(|e| e.to_string())?;
    let pool: Pool =
      serde_json::from_str(&raw).map_err(|e| format!("{}: {e}", cfg_path.display()))?;
    // oauth: Credentials liegen im Keychain, dessen Prüfung wäre ein
    // security-Aufruf pro Pool im 3-s-Polling — deshalb hier immer true.
    let has_credentials = match pool.credential_type.as_str() {
      "apikey" => fs::read_to_string(entry.path().join(APIKEY_FILE))
        .map(|s| !s.trim().is_empty())
        .unwrap_or(false),
      _ => true,
    };
    let id = entry.file_name().to_string_lossy().into_owned();
    let projects = projects_using_pool(paths, &id)?;

fn list_pools_in(paths: &Paths, store: &dyn ApikeyStore) -> Result<Vec<PoolInfo>, String> {
  let mut pools = Vec::new();
  let entries = fs::read_dir(paths.pools_dir()).map_err(|e| e.to_string())?;
  for entry in entries {
    let entry = entry.map_err(|e| e.to_string())?;
    let cfg_path = entry.path().join(POOL_FILE);
    if !cfg_path.is_file() {
      continue;
    }
    let raw = fs::read_to_string(&cfg_path).map_err(|e| e.to_string())?;
    let pool: Pool =
      serde_json::from_str(&raw).map_err(|e| format!("{}: {e}", cfg_path.display()))?;
    let id = entry.file_name().to_string_lossy().into_owned();
    // oauth: Credentials liegen in claudes eigenem Keychain-Eintrag, dessen
    // Prüfung wäre ein security-Aufruf pro Pool im 3-s-Polling — immer true.
    // apikey: Store-Eintrag (nativer API-Call) oder Fallback-Datei.
    let has_credentials = match pool.credential_type.as_str() {
      "apikey" => {
        store.has(&id)?
          || fs::read_to_string(entry.path().join(APIKEY_FILE))
            .map(|s| !s.trim().is_empty())
            .unwrap_or(false)
      }
      _ => true,
    };
    let projects = projects_using_pool(paths, &id)?;