Pools: Anzeigename von Pool-ID (UUID-Ordner) getrennt + Umbenennen; API-Keys in Keychain/Keyring, Datei-Fallback nur nach Rückfrage

This commit is contained in:
marcus.hinz
2026-07-05 12:16:16 +02:00
parent 23aa315b8f
commit be6c56d7eb
6 changed files with 855 additions and 148 deletions
+96 -3
View File
@@ -33,6 +33,7 @@ name = "ai-control"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"keyring",
"log", "log",
"objc2", "objc2",
"objc2-app-kit", "objc2-app-kit",
@@ -46,6 +47,7 @@ dependencies = [
"tauri-plugin-autostart", "tauri-plugin-autostart",
"tauri-plugin-dialog", "tauri-plugin-dialog",
"tauri-plugin-log", "tauri-plugin-log",
"uuid",
] ]
[[package]] [[package]]
@@ -494,6 +496,16 @@ dependencies = [
"version_check", "version_check",
] ]
[[package]]
name = "core-foundation"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]] [[package]]
name = "core-foundation" name = "core-foundation"
version = "0.10.1" version = "0.10.1"
@@ -517,7 +529,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97"
dependencies = [ dependencies = [
"bitflags 2.13.0", "bitflags 2.13.0",
"core-foundation", "core-foundation 0.10.1",
"core-graphics-types", "core-graphics-types",
"foreign-types", "foreign-types",
"libc", "libc",
@@ -530,7 +542,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
dependencies = [ dependencies = [
"bitflags 2.13.0", "bitflags 2.13.0",
"core-foundation", "core-foundation 0.10.1",
"libc", "libc",
] ]
@@ -661,6 +673,16 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "dbus-secret-service"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "708b509edf7889e53d7efb0ffadd994cc6c2345ccb62f55cfd6b0682165e4fa6"
dependencies = [
"dbus",
"zeroize",
]
[[package]] [[package]]
name = "deranged" name = "deranged"
version = "0.5.8" version = "0.5.8"
@@ -1820,6 +1842,21 @@ dependencies = [
"unicode-segmentation", "unicode-segmentation",
] ]
[[package]]
name = "keyring"
version = "3.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c"
dependencies = [
"byteorder",
"dbus-secret-service",
"log",
"security-framework 2.11.1",
"security-framework 3.7.0",
"windows-sys 0.60.2",
"zeroize",
]
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.5.0" version = "1.5.0"
@@ -2965,6 +3002,42 @@ version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "security-framework"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [
"bitflags 2.13.0",
"core-foundation 0.9.4",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework"
version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
dependencies = [
"bitflags 2.13.0",
"core-foundation 0.10.1",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]] [[package]]
name = "selectors" name = "selectors"
version = "0.36.1" version = "0.36.1"
@@ -3394,7 +3467,7 @@ checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9"
dependencies = [ dependencies = [
"bitflags 2.13.0", "bitflags 2.13.0",
"block2", "block2",
"core-foundation", "core-foundation 0.10.1",
"core-graphics", "core-graphics",
"crossbeam-channel", "crossbeam-channel",
"dbus", "dbus",
@@ -5083,6 +5156,26 @@ dependencies = [
"synstructure", "synstructure",
] ]
[[package]]
name = "zeroize"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
dependencies = [
"zeroize_derive",
]
[[package]]
name = "zeroize_derive"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.118",
]
[[package]] [[package]]
name = "zerotrie" name = "zerotrie"
version = "0.2.4" version = "0.2.4"
+2
View File
@@ -28,6 +28,8 @@ tauri-plugin-dialog = "2"
base64 = "0.22" base64 = "0.22"
sha2 = "0.10" sha2 = "0.10"
portable-pty = "0.9" portable-pty = "0.9"
uuid = { version = "1", features = ["v4"] }
keyring = { version = "3", features = ["apple-native", "sync-secret-service", "windows-native"] }
[target.'cfg(target_os = "macos")'.dependencies] [target.'cfg(target_os = "macos")'.dependencies]
objc2 = "0.6" objc2 = "0.6"
+578 -101
View File
File diff suppressed because it is too large Load Diff
+110 -35
View File
@@ -6,6 +6,7 @@ import { useI18n } from "vue-i18n";
const { t } = useI18n(); const { t } = useI18n();
interface Pool { interface Pool {
id: string;
name: string; name: string;
credentialType: string; credentialType: string;
projects: string[]; projects: string[];
@@ -21,6 +22,8 @@ type Mode = "oauth" | "apikey";
const dialog = ref<{ mode: Mode; editing: boolean } | null>(null); const dialog = ref<{ mode: Mode; editing: boolean } | null>(null);
const dName = ref(""); const dName = ref("");
const dKey = ref(""); const dKey = ref("");
// Ziel-Pool (ID) beim Key-Ändern; bei Neuanlage leer.
const dPool = ref("");
const dialogTitle = computed(() => { const dialogTitle = computed(() => {
if (!dialog.value) return ""; if (!dialog.value) return "";
@@ -32,13 +35,15 @@ const dialogTitle = computed(() => {
function openNew(mode: Mode) { function openNew(mode: Mode) {
dName.value = ""; dName.value = "";
dKey.value = ""; dKey.value = "";
dPool.value = "";
error.value = ""; error.value = "";
dialog.value = { mode, editing: false }; dialog.value = { mode, editing: false };
} }
function openEditKey(pool: string) { function openEditKey(pool: Pool) {
dName.value = pool; dName.value = pool.name;
dKey.value = ""; dKey.value = "";
dPool.value = pool.id;
error.value = ""; error.value = "";
dialog.value = { mode: "apikey", editing: true }; dialog.value = { mode: "apikey", editing: true };
} }
@@ -57,7 +62,11 @@ async function refresh() {
} }
} }
async function submit() { // Zweistufig: erster Versuch ohne Datei-Erlaubnis; meldet das Backend
// keychain-unavailable, fragt ein Dialog nach und wiederholt mit allowFile.
const fileConfirm = ref(false);
async function submit(allowFile = false) {
const d = dialog.value; const d = dialog.value;
if (!d) return; if (!d) return;
busy.value = true; busy.value = true;
@@ -66,12 +75,44 @@ async function submit() {
if (d.mode === "oauth") { if (d.mode === "oauth") {
await invoke("create_oauth_pool", { name: dName.value }); await invoke("create_oauth_pool", { name: dName.value });
} else if (d.editing) { } else if (d.editing) {
await invoke("set_apikey", { pool: dName.value, key: dKey.value }); await invoke("set_apikey", { pool: dPool.value, key: dKey.value, allowFile });
} else { } else {
await invoke("create_apikey_pool", { name: dName.value, key: dKey.value }); await invoke("create_apikey_pool", { name: dName.value, key: dKey.value, allowFile });
} }
dialog.value = null; dialog.value = null;
await refresh(); await refresh();
} catch (e) {
if (String(e) === "keychain-unavailable") {
fileConfirm.value = true;
} else {
error.value = String(e);
}
} finally {
busy.value = false;
}
}
async function confirmFileFallback() {
fileConfirm.value = false;
await submit(true);
}
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) { } catch (e) {
error.value = String(e); error.value = String(e);
} finally { } finally {
@@ -79,25 +120,23 @@ async function submit() {
} }
} }
const deleteName = ref<string | null>(null); const renamePool = ref<Pool | null>(null);
const rName = ref("");
const deleteProjects = computed( function openRename(pool: Pool) {
() => pools.value.find((p) => p.name === deleteName.value)?.projects ?? [],
);
function askDelete(pool: string) {
error.value = ""; error.value = "";
deleteName.value = pool; rName.value = pool.name;
renamePool.value = pool;
} }
async function confirmDelete() { async function confirmRename() {
const name = deleteName.value; const pool = renamePool.value;
if (!name) return; if (!pool) return;
busy.value = true; busy.value = true;
error.value = ""; error.value = "";
try { try {
await invoke("delete_pool", { name }); await invoke("rename_pool", { pool: pool.id, name: rName.value });
deleteName.value = null; renamePool.value = null;
await refresh(); await refresh();
} catch (e) { } catch (e) {
error.value = String(e); error.value = String(e);
@@ -112,17 +151,17 @@ interface Project {
running: boolean; running: boolean;
} }
const reloginPool = ref<string | null>(null); const reloginPool = ref<Pool | null>(null);
const reloginHasEntry = ref(false); const reloginHasEntry = ref(false);
const reloginRunning = ref<string[]>([]); const reloginRunning = ref<string[]>([]);
async function askRelogin(pool: string) { async function askRelogin(pool: Pool) {
error.value = ""; error.value = "";
try { try {
reloginHasEntry.value = await invoke<boolean>("keychain_status", { pool }); reloginHasEntry.value = await invoke<boolean>("keychain_status", { pool: pool.id });
const projects = await invoke<Project[]>("list_projects"); const projects = await invoke<Project[]>("list_projects");
reloginRunning.value = projects reloginRunning.value = projects
.filter((p) => p.pool === pool && p.running) .filter((p) => p.pool === pool.id && p.running)
.map((p) => p.name); .map((p) => p.name);
reloginPool.value = pool; reloginPool.value = pool;
} catch (e) { } catch (e) {
@@ -136,7 +175,7 @@ async function confirmRelogin() {
busy.value = true; busy.value = true;
error.value = ""; error.value = "";
try { try {
await invoke("oauth_login", { pool }); await invoke("oauth_login", { pool: pool.id });
reloginPool.value = null; reloginPool.value = null;
await refresh(); await refresh();
} catch (e) { } catch (e) {
@@ -178,7 +217,7 @@ onMounted(refresh);
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="p in pools" :key="p.name"> <tr v-for="p in pools" :key="p.id">
<td class="cell-name"> <td class="cell-name">
<strong>{{ p.name }}</strong> <strong>{{ p.name }}</strong>
</td> </td>
@@ -197,13 +236,16 @@ onMounted(refresh);
</td> </td>
<td class="cell-actions"> <td class="cell-actions">
<span class="row-actions"> <span class="row-actions">
<button :disabled="busy" @click="openRename(p)">
{{ $t("pools.rename") }}
</button>
<template v-if="p.credentialType === 'oauth'"> <template v-if="p.credentialType === 'oauth'">
<button class="w-action" :disabled="busy" @click="askRelogin(p.name)"> <button class="w-action" :disabled="busy" @click="askRelogin(p)">
{{ $t("pools.relogin") }} {{ $t("pools.relogin") }}
</button> </button>
</template> </template>
<template v-else> <template v-else>
<button class="w-action" :disabled="busy" @click="openEditKey(p.name)"> <button class="w-action" :disabled="busy" @click="openEditKey(p)">
{{ p.hasCredentials ? $t("pools.changeKey") : $t("pools.insertKey") }} {{ p.hasCredentials ? $t("pools.changeKey") : $t("pools.insertKey") }}
</button> </button>
</template> </template>
@@ -211,7 +253,7 @@ onMounted(refresh);
<button <button
class="danger" class="danger"
:disabled="busy || p.running.length > 0" :disabled="busy || p.running.length > 0"
@click="askDelete(p.name)" @click="askDelete(p)"
> >
{{ $t("pools.delete") }} {{ $t("pools.delete") }}
</button> </button>
@@ -229,12 +271,12 @@ onMounted(refresh);
<p v-else class="empty">{{ $t("pools.empty") }}</p> <p v-else class="empty">{{ $t("pools.empty") }}</p>
<div v-if="dialog" class="overlay" @click.self="closeDialog"> <div v-if="dialog" class="overlay" @click.self="closeDialog">
<form class="dialog" @submit.prevent="submit"> <form class="dialog" @submit.prevent="submit()">
<h3>{{ dialogTitle }}</h3> <h3>{{ dialogTitle }}</h3>
<label v-if="!dialog.editing" class="field"> <label v-if="!dialog.editing" class="field">
{{ $t("pools.name") }} {{ $t("pools.name") }}
<input v-model="dName" placeholder="z. B. privateDefault" autofocus required /> <input v-model="dName" placeholder="z. B. private" autofocus required />
</label> </label>
<label v-if="dialog.mode === 'apikey'" class="field"> <label v-if="dialog.mode === 'apikey'" class="field">
@@ -269,7 +311,7 @@ onMounted(refresh);
@click.self="busy ? undefined : (reloginPool = null)" @click.self="busy ? undefined : (reloginPool = null)"
> >
<div class="dialog"> <div class="dialog">
<h3>{{ $t("pools.reloginTitle", { name: reloginPool }) }}</h3> <h3>{{ $t("pools.reloginTitle", { name: reloginPool.name }) }}</h3>
<template v-if="reloginRunning.length"> <template v-if="reloginRunning.length">
<p class="hint">{{ $t("pools.reloginBlocked") }}</p> <p class="hint">{{ $t("pools.reloginBlocked") }}</p>
<ul class="affected"> <ul class="affected">
@@ -285,7 +327,7 @@ onMounted(refresh);
<p class="hint"> <p class="hint">
{{ {{
reloginHasEntry reloginHasEntry
? $t("pools.reloginWarning", { name: reloginPool }) ? $t("pools.reloginWarning", { name: reloginPool.name })
: $t("pools.reloginNoEntry") : $t("pools.reloginNoEntry")
}} }}
</p> </p>
@@ -301,18 +343,18 @@ onMounted(refresh);
</div> </div>
</div> </div>
<div v-if="deleteName" class="overlay" @click.self="deleteName = null"> <div v-if="deletePool" class="overlay" @click.self="deletePool = null">
<div class="dialog"> <div class="dialog">
<h3>{{ $t("pools.deletePool") }}</h3> <h3>{{ $t("pools.deletePool") }}</h3>
<p class="hint">{{ $t("pools.deleteWarning", { name: deleteName }) }}</p> <p class="hint">{{ $t("pools.deleteWarning", { name: deletePool.name }) }}</p>
<template v-if="deleteProjects.length"> <template v-if="deletePool.projects.length">
<p class="hint">{{ $t("pools.deleteUnassigns") }}</p> <p class="hint">{{ $t("pools.deleteUnassigns") }}</p>
<ul class="affected"> <ul class="affected">
<li v-for="name in deleteProjects" :key="name">{{ name }}</li> <li v-for="name in deletePool.projects" :key="name">{{ name }}</li>
</ul> </ul>
</template> </template>
<div class="actions"> <div class="actions">
<button type="button" :disabled="busy" @click="deleteName = null"> <button type="button" :disabled="busy" @click="deletePool = null">
{{ $t("pools.cancel") }} {{ $t("pools.cancel") }}
</button> </button>
<button class="danger" :disabled="busy" @click="confirmDelete"> <button class="danger" :disabled="busy" @click="confirmDelete">
@@ -321,4 +363,37 @@ onMounted(refresh);
</div> </div>
</div> </div>
</div> </div>
<div v-if="fileConfirm" class="overlay">
<div class="dialog">
<h3>{{ $t("pools.keychainUnavailableTitle") }}</h3>
<p class="hint">{{ $t("pools.keychainUnavailable") }}</p>
<div class="actions">
<button type="button" :disabled="busy" @click="fileConfirm = false">
{{ $t("pools.cancel") }}
</button>
<button class="danger" :disabled="busy" @click="confirmFileFallback">
{{ $t("pools.storeAsFile") }}
</button>
</div>
</div>
</div>
<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>
<label class="field">
{{ $t("pools.name") }}
<input v-model="rName" autofocus required />
</label>
<div class="actions">
<button type="button" :disabled="busy" @click="renamePool = null">
{{ $t("pools.cancel") }}
</button>
<button type="submit" class="primary" :disabled="busy">
{{ $t("pools.save") }}
</button>
</div>
</form>
</div>
</template> </template>
+53 -5
View File
@@ -12,6 +12,7 @@ interface Project {
} }
interface Pool { interface Pool {
id: string;
name: string; name: string;
credentialType: string; credentialType: string;
hasCredentials: boolean; hasCredentials: boolean;
@@ -19,10 +20,16 @@ interface Pool {
function poolReady(p: Project): boolean { function poolReady(p: Project): boolean {
if (!p.pool) return false; if (!p.pool) return false;
const pool = pools.value.find((x) => x.name === p.pool); const pool = pools.value.find((x) => x.id === p.pool);
return pool !== undefined && pool.hasCredentials; 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;
}
function startBlockedReason(p: Project): string | undefined { function startBlockedReason(p: Project): string | undefined {
if (poolReady(p)) return undefined; if (poolReady(p)) return undefined;
return p.pool ? "projects.needsKey" : "projects.needsPool"; return p.pool ? "projects.needsKey" : "projects.needsPool";
@@ -32,6 +39,35 @@ const projects = ref<Project[]>([]);
const pools = ref<Pool[]>([]); const pools = ref<Pool[]>([]);
const error = ref(""); 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() { async function refresh() {
try { try {
projects.value = await invoke<Project[]>("list_projects"); projects.value = await invoke<Project[]>("list_projects");
@@ -40,6 +76,7 @@ async function refresh() {
} catch (e) { } catch (e) {
error.value = String(e); error.value = String(e);
} }
await loadIcons();
} }
async function stop(project: Project) { async function stop(project: Project) {
@@ -103,6 +140,14 @@ const THEME_NAMES: [string, string][] = [
["solarized-dark", "Solarized Dark"], ["solarized-dark", "Solarized Dark"],
["gruvbox", "Gruvbox"], ["gruvbox", "Gruvbox"],
["one-dark", "One Dark"], ["one-dark", "One Dark"],
["nord", "Nord"],
["tokyo-night", "Tokyo Night"],
["monokai", "Monokai"],
["rose-pine", "Rosé Pine"],
["everforest", "Everforest"],
["solarized-light", "Solarized Light"],
["catppuccin-latte", "Catppuccin Latte"],
["one-light", "One Light"],
]; ];
interface TerminalSettings { interface TerminalSettings {
@@ -292,13 +337,16 @@ onUnmounted(() => window.clearInterval(timer));
<span class="dot" :class="{ on: p.running }"></span> <span class="dot" :class="{ on: p.running }"></span>
</td> </td>
<td class="cell-name"> <td class="cell-name">
<span class="name-row">
<img v-if="projIcon(p)" class="proj-icon" :src="projIcon(p)" />
<strong>{{ p.name }}</strong> <strong>{{ p.name }}</strong>
</span>
<small>{{ p.path }}</small> <small>{{ p.path }}</small>
</td> </td>
<td> <td>
<select :value="p.pool ?? ''" @change="assign(p, $event)"> <select :value="p.pool ?? ''" @change="assign(p, $event)">
<option value="" disabled>{{ $t("projects.noPool") }}</option> <option value="" disabled>{{ $t("projects.noPool") }}</option>
<option v-for="pool in pools" :key="pool.name" :value="pool.name"> <option v-for="pool in pools" :key="pool.id" :value="pool.id">
{{ pool.name }} {{ pool.name }}
</option> </option>
</select> </select>
@@ -349,7 +397,7 @@ onUnmounted(() => window.clearInterval(timer));
{{ $t("projects.pool") }} {{ $t("projects.pool") }}
<select v-model="wizard.pool"> <select v-model="wizard.pool">
<option value="">{{ $t("projects.noPool") }}</option> <option value="">{{ $t("projects.noPool") }}</option>
<option v-for="pool in pools" :key="pool.name" :value="pool.name"> <option v-for="pool in pools" :key="pool.id" :value="pool.id">
{{ pool.name }} {{ pool.name }}
</option> </option>
</select> </select>
@@ -493,9 +541,9 @@ onUnmounted(() => window.clearInterval(timer));
<h3>{{ $t("projects.stillRunning", { name: pending.name }) }}</h3> <h3>{{ $t("projects.stillRunning", { name: pending.name }) }}</h3>
<p>{{ $t("projects.poolChangeSaved") }}</p> <p>{{ $t("projects.poolChangeSaved") }}</p>
<div class="pools"> <div class="pools">
<span class="chip">{{ pending.from ?? $t("projects.noPoolAssigned") }}</span> <span class="chip">{{ poolName(pending.from) ?? $t("projects.noPoolAssigned") }}</span>
<span class="arrow"></span> <span class="arrow"></span>
<span class="chip new">{{ pending.to }}</span> <span class="chip new">{{ poolName(pending.to) }}</span>
</div> </div>
<p class="hint">{{ $t("projects.restartHint") }}</p> <p class="hint">{{ $t("projects.restartHint") }}</p>
<div class="actions"> <div class="actions">
+14 -2
View File
@@ -33,7 +33,7 @@ const de = {
poolChangeSaved: poolChangeSaved:
"Der Pool-Wechsel ist gespeichert. Die laufende Session arbeitet aber weiter im alten Pool — der neue gilt erst ab dem nächsten Start.", "Der Pool-Wechsel ist gespeichert. Die laufende Session arbeitet aber weiter im alten Pool — der neue gilt erst ab dem nächsten Start.",
restartHint: restartHint:
"Der Neustart beendet die laufende Session. Der automatische commit+push von claude-sync nach Sessionende entfällt dabei — Ghostty fragt bei laufendem Prozess vor dem Beenden noch einmal nach.", "Der Neustart beendet die laufende Session. Der automatische commit+push von claude-sync nach Sessionende entfällt dabei.",
keepRunning: "Weiterlaufen lassen", keepRunning: "Weiterlaufen lassen",
restartNow: "Jetzt neu starten", restartNow: "Jetzt neu starten",
restarting: "Starte neu …", restarting: "Starte neu …",
@@ -77,6 +77,8 @@ const de = {
newOauth: "+ oAuth", newOauth: "+ oAuth",
newApikey: "+ apiKey", newApikey: "+ apiKey",
relogin: "Zurücksetzen", relogin: "Zurücksetzen",
rename: "Umbenennen",
renameTitle: "{name} umbenennen",
changeKey: "Key ändern", changeKey: "Key ändern",
insertKey: "Key eintragen", insertKey: "Key eintragen",
delete: "Löschen", delete: "Löschen",
@@ -106,6 +108,10 @@ const de = {
"Kein Schlüsselbund-Eintrag vorhanden — beim nächsten Start meldet sich claude ohnehin per /login an.", "Kein Schlüsselbund-Eintrag vorhanden — beim nächsten Start meldet sich claude ohnehin per /login an.",
reloginBlocked: reloginBlocked:
"Zurücksetzen ist nur bei ungenutztem Pool möglich. Diese Projekte laufen noch:", "Zurücksetzen ist nur bei ungenutztem Pool möglich. Diese Projekte laufen noch:",
keychainUnavailableTitle: "Keine sichere Ablage",
keychainUnavailable:
"Keychain/Keyring ist nicht verfügbar. Der Key kann stattdessen ungesichert als Datei im Pool-Ordner liegen (0600).",
storeAsFile: "Als Datei ablegen",
}, },
}; };
@@ -142,7 +148,7 @@ const en: typeof de = {
poolChangeSaved: poolChangeSaved:
"The pool change is saved. The running session keeps working in the old pool — the new one applies from the next start.", "The pool change is saved. The running session keeps working in the old pool — the new one applies from the next start.",
restartHint: restartHint:
"Restarting ends the running session. The automatic commit+push by claude-sync after the session is skipped — Ghostty asks once more before quitting a running process.", "Restarting ends the running session. The automatic commit+push by claude-sync after the session is skipped.",
keepRunning: "Keep running", keepRunning: "Keep running",
restartNow: "Restart now", restartNow: "Restart now",
restarting: "Restarting …", restarting: "Restarting …",
@@ -186,6 +192,8 @@ const en: typeof de = {
newOauth: "+ oAuth", newOauth: "+ oAuth",
newApikey: "+ apiKey", newApikey: "+ apiKey",
relogin: "Reset", relogin: "Reset",
rename: "Rename",
renameTitle: "Rename {name}",
changeKey: "Change key", changeKey: "Change key",
insertKey: "Insert key", insertKey: "Insert key",
delete: "Delete", delete: "Delete",
@@ -215,6 +223,10 @@ const en: typeof de = {
"No keychain entry present — claude signs in via /login on the next start anyway.", "No keychain entry present — claude signs in via /login on the next start anyway.",
reloginBlocked: reloginBlocked:
"Resetting requires an unused pool. These projects are still running:", "Resetting requires an unused pool. These projects are still running:",
keychainUnavailableTitle: "No secure storage",
keychainUnavailable:
"Keychain/keyring is unavailable. The key can instead be stored unprotected as a file in the pool folder (0600).",
storeAsFile: "Store as file",
}, },
}; };