Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| be6c56d7eb | |||
| 23aa315b8f |
@@ -8,4 +8,18 @@ export RUSTUP_HOME="$HOME/tools/.rustup"
|
|||||||
export PATH="$CARGO_HOME/bin:$PATH"
|
export PATH="$CARGO_HOME/bin:$PATH"
|
||||||
|
|
||||||
cd "$DIR"
|
cd "$DIR"
|
||||||
npm run tauri build -- "$@"
|
npm run tauri build -- --bundles app "$@"
|
||||||
|
|
||||||
|
# Dev-Bundle in ein .noindex-Verzeichnis schieben und aus Launch Services
|
||||||
|
# austragen — Spotlight indiziert .noindex-Ordner nicht, damit erscheint im
|
||||||
|
# Starter/Spotlight nur die installierte Kopie unter ~/Applications.
|
||||||
|
BUNDLE="$DIR/src-tauri/target/release/bundle/macos"
|
||||||
|
NOINDEX="$DIR/src-tauri/target/release/bundle/macos.noindex"
|
||||||
|
LSREGISTER=/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister
|
||||||
|
# lsregister -u meldet -10814, wenn das Bundle nicht registriert ist —
|
||||||
|
# erwartetes Ergebnis, kein Fehler.
|
||||||
|
"$LSREGISTER" -u "$BUNDLE/ai-control.app" || true
|
||||||
|
rm -rf "$NOINDEX/ai-control.app"
|
||||||
|
mkdir -p "$NOINDEX"
|
||||||
|
mv "$BUNDLE/ai-control.app" "$NOINDEX/"
|
||||||
|
"$LSREGISTER" -u "$NOINDEX/ai-control.app" || true
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 1012 B After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 956 B After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 867 B After Width: | Height: | Size: 747 B |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
@@ -123,17 +123,46 @@ pub fn term_start(
|
|||||||
|
|
||||||
let label = window.label().to_string();
|
let label = window.label().to_string();
|
||||||
let app = window.app_handle().clone();
|
let app = window.app_handle().clone();
|
||||||
|
|
||||||
|
// Output gebündelt emittieren statt pro read(): der Reader schiebt Chunks
|
||||||
|
// in einen Channel, der Emitter sammelt alles, was innerhalb von 8 ms
|
||||||
|
// ankommt, zu einem Event zusammen. Bei Output-Fluten (Scrollen, TUI-
|
||||||
|
// Redraws) sinkt die Zahl der IPC-Events um Größenordnungen; die 8 ms
|
||||||
|
// Zusatzlatenz beim Tasten-Echo liegen unter einem Frame.
|
||||||
|
let (tx, rx) = std::sync::mpsc::channel::<Vec<u8>>();
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
let mut buf = [0u8; 8192];
|
let mut buf = [0u8; 8192];
|
||||||
loop {
|
loop {
|
||||||
match reader.read(&mut buf) {
|
match reader.read(&mut buf) {
|
||||||
Ok(0) | Err(_) => break,
|
Ok(0) | Err(_) => break,
|
||||||
Ok(n) => {
|
Ok(n) => {
|
||||||
let _ = app.emit_to(&label, "pty-output", STANDARD.encode(&buf[..n]));
|
if tx.send(buf[..n].to_vec()).is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let _ = child.wait();
|
let _ = child.wait();
|
||||||
|
// tx wird hier gedroppt — der Emitter sieht den geschlossenen Channel
|
||||||
|
// erst, nachdem er alle Rest-Chunks emittiert hat.
|
||||||
|
});
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
const FLUSH: std::time::Duration = std::time::Duration::from_millis(8);
|
||||||
|
while let Ok(first) = rx.recv() {
|
||||||
|
let mut chunk = first;
|
||||||
|
let deadline = std::time::Instant::now() + FLUSH;
|
||||||
|
loop {
|
||||||
|
let now = std::time::Instant::now();
|
||||||
|
if now >= deadline {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
match rx.recv_timeout(deadline - now) {
|
||||||
|
Ok(more) => chunk.extend_from_slice(&more),
|
||||||
|
Err(_) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _ = app.emit_to(&label, "pty-output", STANDARD.encode(&chunk));
|
||||||
|
}
|
||||||
let _ = app.emit_to(&label, "pty-exit", ());
|
let _ = app.emit_to(&label, "pty-exit", ());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -387,6 +387,19 @@ tbody tr:hover {
|
|||||||
background: color-mix(in srgb, var(--mantle) 60%, transparent);
|
background: color-mix(in srgb, var(--mantle) 60%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.name-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.proj-icon {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 4px;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
.cell-name strong {
|
.cell-name strong {
|
||||||
font-family: var(--mono);
|
font-family: var(--mono);
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { LigaturesAddon } from "@xterm/addon-ligatures";
|
|||||||
import "@xterm/xterm/css/xterm.css";
|
import "@xterm/xterm/css/xterm.css";
|
||||||
import "@fontsource/jetbrains-mono/400.css";
|
import "@fontsource/jetbrains-mono/400.css";
|
||||||
import "@fontsource/jetbrains-mono/500.css";
|
import "@fontsource/jetbrains-mono/500.css";
|
||||||
|
import "@fontsource/jetbrains-mono/600.css";
|
||||||
import "@fontsource/jetbrains-mono/700.css";
|
import "@fontsource/jetbrains-mono/700.css";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||||
@@ -28,8 +29,9 @@ window.addEventListener("unhandledrejection", (e) =>
|
|||||||
const project = new URLSearchParams(location.search).get("project")!;
|
const project = new URLSearchParams(location.search).get("project")!;
|
||||||
const win = getCurrentWebviewWindow();
|
const win = getCurrentWebviewWindow();
|
||||||
|
|
||||||
// Fensterhintergründe je Theme stehen zusätzlich in terminal.rs
|
// Fensterhintergrund beim Öffnen kommt aus terminal.rs (theme_background);
|
||||||
// (theme_background) — beide Stellen müssen zusammenpassen.
|
// dort nicht gepflegte Themes fallen auf Mocha zurück (nur kurzer Moment
|
||||||
|
// bis die Webview lädt).
|
||||||
const THEMES: Record<
|
const THEMES: Record<
|
||||||
string,
|
string,
|
||||||
{ header: string; border: string; xterm: Record<string, string> }
|
{ header: string; border: string; xterm: Record<string, string> }
|
||||||
@@ -169,6 +171,222 @@ const THEMES: Record<
|
|||||||
brightWhite: "#ffffff",
|
brightWhite: "#ffffff",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"solarized-light": {
|
||||||
|
header: "#eee8d5",
|
||||||
|
border: "#93a1a1",
|
||||||
|
xterm: {
|
||||||
|
background: "#fdf6e3",
|
||||||
|
foreground: "#657b83",
|
||||||
|
cursor: "#657b83",
|
||||||
|
cursorAccent: "#fdf6e3",
|
||||||
|
selectionBackground: "#eee8d5b0",
|
||||||
|
black: "#073642",
|
||||||
|
red: "#dc322f",
|
||||||
|
green: "#859900",
|
||||||
|
yellow: "#b58900",
|
||||||
|
blue: "#268bd2",
|
||||||
|
magenta: "#d33682",
|
||||||
|
cyan: "#2aa198",
|
||||||
|
white: "#eee8d5",
|
||||||
|
brightBlack: "#002b36",
|
||||||
|
brightRed: "#cb4b16",
|
||||||
|
brightGreen: "#586e75",
|
||||||
|
brightYellow: "#657b83",
|
||||||
|
brightBlue: "#839496",
|
||||||
|
brightMagenta: "#6c71c4",
|
||||||
|
brightCyan: "#93a1a1",
|
||||||
|
brightWhite: "#fdf6e3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"catppuccin-latte": {
|
||||||
|
header: "#e6e9ef",
|
||||||
|
border: "#ccd0da",
|
||||||
|
xterm: {
|
||||||
|
background: "#eff1f5",
|
||||||
|
foreground: "#4c4f69",
|
||||||
|
cursor: "#dc8a78",
|
||||||
|
cursorAccent: "#eff1f5",
|
||||||
|
selectionBackground: "#acb0be80",
|
||||||
|
black: "#5c5f77",
|
||||||
|
red: "#d20f39",
|
||||||
|
green: "#40a02b",
|
||||||
|
yellow: "#df8e1d",
|
||||||
|
blue: "#1e66f5",
|
||||||
|
magenta: "#ea76cb",
|
||||||
|
cyan: "#179299",
|
||||||
|
white: "#acb0be",
|
||||||
|
brightBlack: "#6c6f85",
|
||||||
|
brightRed: "#d20f39",
|
||||||
|
brightGreen: "#40a02b",
|
||||||
|
brightYellow: "#df8e1d",
|
||||||
|
brightBlue: "#1e66f5",
|
||||||
|
brightMagenta: "#ea76cb",
|
||||||
|
brightCyan: "#179299",
|
||||||
|
brightWhite: "#bcc0cc",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"one-light": {
|
||||||
|
header: "#eaeaeb",
|
||||||
|
border: "#d4d4d4",
|
||||||
|
xterm: {
|
||||||
|
background: "#fafafa",
|
||||||
|
foreground: "#383a42",
|
||||||
|
cursor: "#526eff",
|
||||||
|
cursorAccent: "#fafafa",
|
||||||
|
selectionBackground: "#e5e5e6b0",
|
||||||
|
black: "#383a42",
|
||||||
|
red: "#e45649",
|
||||||
|
green: "#50a14f",
|
||||||
|
yellow: "#c18401",
|
||||||
|
blue: "#4078f2",
|
||||||
|
magenta: "#a626a4",
|
||||||
|
cyan: "#0184bc",
|
||||||
|
white: "#a0a1a7",
|
||||||
|
brightBlack: "#696c77",
|
||||||
|
brightRed: "#e45649",
|
||||||
|
brightGreen: "#50a14f",
|
||||||
|
brightYellow: "#c18401",
|
||||||
|
brightBlue: "#4078f2",
|
||||||
|
brightMagenta: "#a626a4",
|
||||||
|
brightCyan: "#0184bc",
|
||||||
|
brightWhite: "#ffffff",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nord: {
|
||||||
|
header: "#3b4252",
|
||||||
|
border: "#4c566a",
|
||||||
|
xterm: {
|
||||||
|
background: "#2e3440",
|
||||||
|
foreground: "#d8dee9",
|
||||||
|
cursor: "#d8dee9",
|
||||||
|
cursorAccent: "#2e3440",
|
||||||
|
selectionBackground: "#434c5e80",
|
||||||
|
black: "#3b4252",
|
||||||
|
red: "#bf616a",
|
||||||
|
green: "#a3be8c",
|
||||||
|
yellow: "#ebcb8b",
|
||||||
|
blue: "#81a1c1",
|
||||||
|
magenta: "#b48ead",
|
||||||
|
cyan: "#88c0d0",
|
||||||
|
white: "#e5e9f0",
|
||||||
|
brightBlack: "#4c566a",
|
||||||
|
brightRed: "#bf616a",
|
||||||
|
brightGreen: "#a3be8c",
|
||||||
|
brightYellow: "#ebcb8b",
|
||||||
|
brightBlue: "#81a1c1",
|
||||||
|
brightMagenta: "#b48ead",
|
||||||
|
brightCyan: "#8fbcbb",
|
||||||
|
brightWhite: "#eceff4",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"tokyo-night": {
|
||||||
|
header: "#16161e",
|
||||||
|
border: "#292e42",
|
||||||
|
xterm: {
|
||||||
|
background: "#1a1b26",
|
||||||
|
foreground: "#c0caf5",
|
||||||
|
cursor: "#c0caf5",
|
||||||
|
cursorAccent: "#1a1b26",
|
||||||
|
selectionBackground: "#28345780",
|
||||||
|
black: "#15161e",
|
||||||
|
red: "#f7768e",
|
||||||
|
green: "#9ece6a",
|
||||||
|
yellow: "#e0af68",
|
||||||
|
blue: "#7aa2f7",
|
||||||
|
magenta: "#bb9af7",
|
||||||
|
cyan: "#7dcfff",
|
||||||
|
white: "#a9b1d6",
|
||||||
|
brightBlack: "#414868",
|
||||||
|
brightRed: "#f7768e",
|
||||||
|
brightGreen: "#9ece6a",
|
||||||
|
brightYellow: "#e0af68",
|
||||||
|
brightBlue: "#7aa2f7",
|
||||||
|
brightMagenta: "#bb9af7",
|
||||||
|
brightCyan: "#7dcfff",
|
||||||
|
brightWhite: "#c0caf5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
monokai: {
|
||||||
|
header: "#1e1f1c",
|
||||||
|
border: "#49483e",
|
||||||
|
xterm: {
|
||||||
|
background: "#272822",
|
||||||
|
foreground: "#f8f8f2",
|
||||||
|
cursor: "#f8f8f2",
|
||||||
|
cursorAccent: "#272822",
|
||||||
|
selectionBackground: "#49483e80",
|
||||||
|
black: "#272822",
|
||||||
|
red: "#f92672",
|
||||||
|
green: "#a6e22e",
|
||||||
|
yellow: "#e6db74",
|
||||||
|
blue: "#66d9ef",
|
||||||
|
magenta: "#ae81ff",
|
||||||
|
cyan: "#a1efe4",
|
||||||
|
white: "#f8f8f2",
|
||||||
|
brightBlack: "#75715e",
|
||||||
|
brightRed: "#f92672",
|
||||||
|
brightGreen: "#a6e22e",
|
||||||
|
brightYellow: "#f4bf75",
|
||||||
|
brightBlue: "#66d9ef",
|
||||||
|
brightMagenta: "#ae81ff",
|
||||||
|
brightCyan: "#a1efe4",
|
||||||
|
brightWhite: "#f9f8f5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"rose-pine": {
|
||||||
|
header: "#1f1d2e",
|
||||||
|
border: "#26233a",
|
||||||
|
xterm: {
|
||||||
|
background: "#191724",
|
||||||
|
foreground: "#e0def4",
|
||||||
|
cursor: "#e0def4",
|
||||||
|
cursorAccent: "#191724",
|
||||||
|
selectionBackground: "#403d5280",
|
||||||
|
black: "#26233a",
|
||||||
|
red: "#eb6f92",
|
||||||
|
green: "#31748f",
|
||||||
|
yellow: "#f6c177",
|
||||||
|
blue: "#9ccfd8",
|
||||||
|
magenta: "#c4a7e7",
|
||||||
|
cyan: "#ebbcba",
|
||||||
|
white: "#e0def4",
|
||||||
|
brightBlack: "#6e6a86",
|
||||||
|
brightRed: "#eb6f92",
|
||||||
|
brightGreen: "#31748f",
|
||||||
|
brightYellow: "#f6c177",
|
||||||
|
brightBlue: "#9ccfd8",
|
||||||
|
brightMagenta: "#c4a7e7",
|
||||||
|
brightCyan: "#ebbcba",
|
||||||
|
brightWhite: "#e0def4",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
everforest: {
|
||||||
|
header: "#232a2e",
|
||||||
|
border: "#475258",
|
||||||
|
xterm: {
|
||||||
|
background: "#2d353b",
|
||||||
|
foreground: "#d3c6aa",
|
||||||
|
cursor: "#d3c6aa",
|
||||||
|
cursorAccent: "#2d353b",
|
||||||
|
selectionBackground: "#47525880",
|
||||||
|
black: "#475258",
|
||||||
|
red: "#e67e80",
|
||||||
|
green: "#a7c080",
|
||||||
|
yellow: "#dbbc7f",
|
||||||
|
blue: "#7fbbb3",
|
||||||
|
magenta: "#d699b6",
|
||||||
|
cyan: "#83c092",
|
||||||
|
white: "#d3c6aa",
|
||||||
|
brightBlack: "#859289",
|
||||||
|
brightRed: "#e67e80",
|
||||||
|
brightGreen: "#a7c080",
|
||||||
|
brightYellow: "#dbbc7f",
|
||||||
|
brightBlue: "#7fbbb3",
|
||||||
|
brightMagenta: "#d699b6",
|
||||||
|
brightCyan: "#83c092",
|
||||||
|
brightWhite: "#d3c6aa",
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
interface Project {
|
interface Project {
|
||||||
@@ -199,7 +417,7 @@ const term = new Terminal({
|
|||||||
fontFamily: '"JetBrains Mono", Menlo, monospace',
|
fontFamily: '"JetBrains Mono", Menlo, monospace',
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeightBold: "600",
|
fontWeightBold: "600",
|
||||||
lineHeight: 1.25,
|
lineHeight: 1.0,
|
||||||
letterSpacing: 0,
|
letterSpacing: 0,
|
||||||
cursorBlink: true,
|
cursorBlink: true,
|
||||||
cursorStyle: "bar",
|
cursorStyle: "bar",
|
||||||
@@ -220,8 +438,10 @@ term.attachCustomKeyEventHandler((e) => {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Font muss geladen sein, bevor WebGL den Glyphen-Atlas aufbaut.
|
// Font muss geladen sein, bevor WebGL den Glyphen-Atlas aufbaut —
|
||||||
|
// beide verwendeten Gewichte (400 normal, 600 bold).
|
||||||
await document.fonts.load('13px "JetBrains Mono"');
|
await document.fonts.load('13px "JetBrains Mono"');
|
||||||
|
await document.fonts.load('600 13px "JetBrains Mono"');
|
||||||
term.open(document.getElementById("term")!);
|
term.open(document.getElementById("term")!);
|
||||||
// Reihenfolge laut Addon-Doku: Ligaturen vor WebGL aktivieren.
|
// Reihenfolge laut Addon-Doku: Ligaturen vor WebGL aktivieren.
|
||||||
term.loadAddon(new LigaturesAddon());
|
term.loadAddon(new LigaturesAddon());
|
||||||
|
|||||||