Pools können auf ein vorhandenes Config-Verzeichnis verweisen

Wer claude schon benutzt, hat einen fertigen Login in ~/.claude. Bisher
begann ai-control trotzdem bei null: jeder Pool war ein neu angelegtes
Verzeichnis, der erste Sessionstart verlangte /login. Ein Pool kann jetzt
stattdessen auf ein bestehendes Verzeichnis zeigen.

pool.json trägt dafür optional `dir` (home-kontrahiert). Der Pool-Ordner
ist dann nur die Hülle für diese Datei; pool_config_dir() löst auf und
ist die einzige Stelle, über die Sessionstart, Verbrauchsstatistik,
Panel-Provisionierung und Keychain-Name gehen.

Zeigt ein Pool auf claudes Default-Verzeichnis, bleibt CLAUDE_CONFIG_DIR
ungesetzt: claude nimmt dieses Verzeichnis von sich aus und damit den
unsuffixierten Keychain-Eintrag, an dem der bestehende Login hängt (am
Keychain gegengeprüft — "Claude Code-credentials" ohne Suffix neben den
suffixierten Pool-Einträgen). Ein Verweis auf ein anderes Verzeichnis
setzt die Variable wie gewohnt.

Das verwiesene Verzeichnis gehört der App nicht: dort entsteht nur die
Panel-Ausstattung (Skill, MCP-Eintrag, Freigabe-Ergänzung, alles
additiv), keine settings.json-Grundausstattung. Löschen trifft nur die
Hülle; Runtime-Symlinks und das Zurücksetzen des Keychain-Eintrags sind
gesperrt. Ein Verzeichnis kann nur ein Pool belegen, Ziele innerhalb von
pools/ sind ausgeschlossen.

In der UI erscheint der Knopf nur, wenn ~/.claude existiert und noch
kein Pool darauf zeigt; die Liste zeigt den Pfad unter dem Namen und
statt "Zurücksetzen" den Hinweis, dass die Anmeldung claude gehört.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
marcusH
2026-07-22 09:43:00 +02:00
parent 6b4afd2bc5
commit df828daea9
16 changed files with 383 additions and 25 deletions
+25 -5
View File
@@ -12,13 +12,18 @@ interface Pool {
projects: string[];
running: string[];
hasCredentials: boolean;
/// Bei verwiesenen Pools das fremde Config-Verzeichnis, sonst nicht gesetzt.
dir?: string;
}
const pools = ref<Pool[]>([]);
const error = ref("");
const busy = ref(false);
type Mode = "oauth" | "apikey";
// Claudes Default-Verzeichnis, solange es existiert und kein Pool darauf zeigt.
const defaultDir = ref<string | null>(null);
type Mode = "oauth" | "apikey" | "reference";
const dialog = ref<{ mode: Mode; editing: boolean } | null>(null);
const dName = ref("");
const dKey = ref("");
@@ -28,12 +33,13 @@ const dPool = ref("");
const dialogTitle = computed(() => {
if (!dialog.value) return "";
if (dialog.value.mode === "oauth") return t("pools.newOauthPool");
if (dialog.value.mode === "reference") return t("pools.newReferencePool");
if (dialog.value.editing) return t("pools.editKey", { name: dName.value });
return t("pools.newApikeyPool");
});
function openNew(mode: Mode) {
dName.value = "";
dName.value = mode === "reference" ? t("pools.referenceDefaultName") : "";
dKey.value = "";
dPool.value = "";
error.value = "";
@@ -56,6 +62,7 @@ function closeDialog() {
async function refresh() {
try {
pools.value = await invoke<Pool[]>("list_pools");
defaultDir.value = await invoke<string | null>("default_config_dir");
error.value = "";
} catch (e) {
error.value = String(e);
@@ -74,6 +81,8 @@ async function submit(allowFile = false) {
try {
if (d.mode === "oauth") {
await invoke("create_oauth_pool", { name: dName.value });
} else if (d.mode === "reference") {
await invoke("create_reference_pool", { name: dName.value, dir: defaultDir.value });
} else if (d.editing) {
await invoke("set_apikey", { pool: dPool.value, key: dKey.value, allowFile });
} else {
@@ -197,6 +206,9 @@ onMounted(refresh);
<button class="primary" :disabled="busy" @click="openNew('apikey')">
{{ $t("pools.newApikey") }}
</button>
<button v-if="defaultDir" :disabled="busy" @click="openNew('reference')">
{{ $t("pools.newReference") }}
</button>
</div>
<p v-if="error" class="error">{{ error }}</p>
@@ -221,6 +233,7 @@ onMounted(refresh);
<tr v-for="p in pools" :key="p.id">
<td class="cell-name">
<strong>{{ p.name }}</strong>
<small v-if="p.dir">{{ p.dir }}</small>
</td>
<td>
<span class="badge" :class="p.credentialType">{{ p.credentialType }}</span>
@@ -240,16 +253,19 @@ onMounted(refresh);
<button :disabled="busy" @click="openRename(p)">
{{ $t("pools.rename") }}
</button>
<template v-if="p.credentialType === 'oauth'">
<template v-if="p.credentialType === 'oauth' && !p.dir">
<button class="w-action" :disabled="busy" @click="askRelogin(p)">
{{ $t("pools.relogin") }}
</button>
</template>
<template v-else>
<template v-else-if="p.credentialType === 'apikey'">
<button class="w-action" :disabled="busy" @click="openEditKey(p)">
{{ p.hasCredentials ? $t("pools.changeKey") : $t("pools.insertKey") }}
</button>
</template>
<!-- Verwiesener Pool: die Anmeldung gehört dem fremden
Verzeichnis, die App fasst sie nicht an. -->
<span v-else class="w-action quiet">{{ $t("pools.referenceLogin") }}</span>
<span class="tip-wrap" :class="{ 'hover-pop': p.running.length }">
<button
class="danger"
@@ -296,12 +312,16 @@ onMounted(refresh);
{{ $t("pools.oauthHint") }}
</p>
<p v-if="dialog.mode === 'reference'" class="hint">
{{ $t("pools.referenceHint", { dir: defaultDir ?? "" }) }}
</p>
<div class="actions">
<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") }}
{{ dialog.mode === "apikey" ? $t("pools.save") : $t("pools.createPool") }}
</button>
</div>
</form>
+1
View File
@@ -44,6 +44,7 @@ describe("Nachrichtentabellen", () => {
"Cache ↓",
"+ oAuth",
"+ apiKey",
"System",
"Terminal",
"Terminal — {name}",
".desktop-Starter",
+12
View File
@@ -130,6 +130,12 @@ const de = {
apiKey: "API-Key",
oauthHint:
"Der Pool wird angelegt; die Anmeldung macht claude beim ersten Start selbst per /login (Browser gegen dein Abo).",
newReference: "+ vorhandene Anmeldung",
newReferencePool: "Vorhandene Anmeldung übernehmen",
referenceDefaultName: "System",
referenceHint:
"Der Pool verweist auf {dir} — claudes eigenes Verzeichnis. Die dortige Anmeldung gilt weiter, ein /login entfällt. Die App legt dort nur den Panel-Zugang an und löscht nichts.",
referenceLogin: "Anmeldung von claude",
cancel: "Abbrechen",
createPool: "Anlegen",
reset: "Zurücksetzen",
@@ -349,6 +355,12 @@ const en: typeof de = {
apiKey: "API key",
oauthHint:
"The pool is created; claude signs in itself on first start via /login (browser against your subscription).",
newReference: "+ existing login",
newReferencePool: "Adopt existing login",
referenceDefaultName: "System",
referenceHint:
"The pool points at {dir} — claude's own directory. The login there keeps working, so no /login is needed. The app only adds panel access there and deletes nothing.",
referenceLogin: "login from claude",
cancel: "Cancel",
createPool: "Create",
reset: "Reset",
+9
View File
@@ -430,6 +430,15 @@ tbody tr:hover {
font-weight: 500;
}
/* Platzhalter statt Aktionsknopf: verwiesene Pools haben keine Anmeldung,
die die App zurücksetzen dürfte. */
.cell-actions .quiet {
color: var(--overlay);
font-size: 0.75rem;
align-self: center;
text-align: center;
}
.cell-name small {
display: block;
font-family: var(--mono);