Tray-Start ohne Pool oeffnet die Pool-Auswahl
Ein Projekt ohne zugewiesenen Pool liefert jetzt den Fehler NO_POOL, statt still claudes Default-CLAUDE_CONFIG_DIR zu uebernehmen. Der Tray-/Popup-Start faengt ihn ab, holt das Hauptfenster nach vorn und oeffnet die Pool-Auswahl (pool-required-Event, ProjectList), mit Tests.
This commit is contained in:
@@ -291,6 +291,18 @@ pub(crate) fn restart_project(app: tauri::AppHandle, project: String) -> Result<
|
||||
terminal::open_terminal(app, project)
|
||||
}
|
||||
|
||||
/// Projekt ohne Pool: Hauptfenster nach vorn holen und dort die Pool-Auswahl
|
||||
/// öffnen. Aus dem Tray heraus gibt es sonst keinen Ort für die Wahl — und
|
||||
/// still den Default zu nehmen ist genau das, was die Pool-Pflicht verhindert.
|
||||
fn request_pool_choice(app: &tauri::AppHandle, project: &str) {
|
||||
use tauri::{Emitter, Manager};
|
||||
if let Some(w) = app.get_webview_window("main") {
|
||||
let _ = w.show();
|
||||
let _ = w.set_focus();
|
||||
}
|
||||
let _ = app.emit("pool-required", project);
|
||||
}
|
||||
|
||||
/// Tray-Klick auf ein Projekt: läuft es, kommt das Terminal-Fenster nach vorn,
|
||||
/// sonst startet es.
|
||||
fn start_or_focus(app: &tauri::AppHandle, project: &str) {
|
||||
@@ -298,11 +310,15 @@ fn start_or_focus(app: &tauri::AppHandle, project: &str) {
|
||||
Some(pid) => crate::platform::focus_terminal(*pid),
|
||||
None => {
|
||||
if let Err(e) = terminal::open_terminal(app.clone(), project.to_string()) {
|
||||
if e == crate::domain::project::NO_POOL {
|
||||
request_pool_choice(app, project);
|
||||
} else {
|
||||
eprintln!("{project} starten: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tray-/Popup-Klick: Projekt starten oder das laufende Terminal fokussieren.
|
||||
#[tauri::command]
|
||||
|
||||
@@ -205,9 +205,20 @@ pub(crate) fn verify_project_dir_in(paths: &Paths, project: &str) -> Result<Path
|
||||
Ok(dir)
|
||||
}
|
||||
|
||||
/// Fehlerkennung „Projekt hat keinen Pool". Der Tray-/Popup-Start fängt genau
|
||||
/// diese ab und öffnet die Pool-Auswahl. Eine Kennung statt Klartext, damit die
|
||||
/// Unterscheidung nicht an einer übersetzbaren Meldung hängt.
|
||||
pub(crate) const NO_POOL: &str = "no-pool";
|
||||
|
||||
/// Pool-Config-Verzeichnis eines Projekts — wird dem Terminal als
|
||||
/// CLAUDE_CONFIG_DIR mitgegeben.
|
||||
///
|
||||
/// Ohne zugewiesenen Pool ist das ein Fehler, kein `Ok(None)`: Sonst startete
|
||||
/// die Session ohne CLAUDE_CONFIG_DIR und übernähme damit stillschweigend
|
||||
/// claudes Default-Verzeichnis samt dessen Login — ein Verzeichnis, das der
|
||||
/// Nutzer nie gewählt hat. `Ok(None)` bleibt allein dem Referenzpool auf
|
||||
/// `~/.claude` vorbehalten: dort ist der Default die ausdrückliche Wahl.
|
||||
///
|
||||
/// Der Pool-Name wird geprüft, obwohl `assign_pool_in` das beim Zuweisen schon
|
||||
/// tut: Die Migration übernimmt Pool-Einträge aus der alten, versionierten
|
||||
/// ai-control.json eines geklonten Repos. Ungeprüft bestimmte ein `../…`
|
||||
@@ -223,7 +234,7 @@ pub(crate) fn project_pool_dir_in(
|
||||
project: &str,
|
||||
) -> Result<Option<PathBuf>, String> {
|
||||
let Some(pool) = project_pool(paths, project)? else {
|
||||
return Ok(None);
|
||||
return Err(NO_POOL.to_string());
|
||||
};
|
||||
check_name(&pool).map_err(|_| format!("ungültiger Pool in der Registry: {pool}"))?;
|
||||
let dir = crate::domain::pool::pool_config_dir(paths, &pool)?;
|
||||
@@ -798,6 +809,18 @@ mod tests {
|
||||
assert!(err.contains("ungültiger Pool"));
|
||||
}
|
||||
|
||||
/// Ohne zugewiesenen Pool gibt es kein CLAUDE_CONFIG_DIR und damit auch
|
||||
/// keine Session: Der Fehler trägt die Kennung, an der der Tray-Start die
|
||||
/// Pool-Auswahl öffnet. Ein stilles `Ok(None)` würde claudes
|
||||
/// Default-Verzeichnis ungefragt übernehmen.
|
||||
#[test]
|
||||
fn projekt_ohne_pool_liefert_fehler() {
|
||||
let p = tmp_paths();
|
||||
create_project(&p, "ohne").unwrap();
|
||||
|
||||
assert_eq!(project_pool_dir_in(&p, "ohne").unwrap_err(), NO_POOL);
|
||||
}
|
||||
|
||||
/// Das Setzen der Terminal-Config kommt aus der Oberfläche und kennt nur
|
||||
/// theme/icon/title — unbekannte Keys müssen trotzdem stehen bleiben.
|
||||
#[test]
|
||||
|
||||
@@ -38,6 +38,10 @@ pub fn open_terminal(app: AppHandle, project: String) -> Result<(), String> {
|
||||
if !dir.is_dir() {
|
||||
return Err(format!("Projektordner fehlt: {}", dir.display()));
|
||||
}
|
||||
// Pool-Pflicht vor dem Fenster: Ohne zugewiesenen Pool gibt es kein
|
||||
// Terminal. Die Prüfung gehört hierher und nicht erst in `term_start` —
|
||||
// dort stünde bereits ein Fenster, das nur noch scheitern könnte.
|
||||
project_pool_dir(&project)?;
|
||||
let bundle_id = app.config().identifier.clone();
|
||||
app
|
||||
.run_on_main_thread(move || crate::platform::yield_activation(&bundle_id))
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, ref } from "vue";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
|
||||
import { open } from "@tauri-apps/plugin-dialog";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
interface Project {
|
||||
id: string;
|
||||
@@ -470,12 +474,69 @@ async function confirmDelete() {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Pool-Pflicht ----------
|
||||
|
||||
// Start ohne zugewiesenen Pool: Der Kern verweigert die Session, die App holt
|
||||
// die Wahl hier nach. Kein Pool bedeutet keinen Start — auch nicht still im
|
||||
// Default-Verzeichnis von claude.
|
||||
const poolRequired = ref<{ id: string; name: string } | null>(null);
|
||||
const poolPick = ref("");
|
||||
// Claudes Default-Verzeichnis, solange kein Pool darauf zeigt: als
|
||||
// ausdrücklich wählbarer Eintrag, nicht als stille Übernahme.
|
||||
const defaultDir = ref<string | null>(null);
|
||||
const DEFAULT_PICK = "__default__";
|
||||
|
||||
async function askForPool(id: string) {
|
||||
await refresh();
|
||||
const p = projects.value.find((x) => x.id === id);
|
||||
if (!p || p.pool) return;
|
||||
try {
|
||||
defaultDir.value = await invoke<string | null>("default_config_dir");
|
||||
} catch {
|
||||
defaultDir.value = null;
|
||||
}
|
||||
poolPick.value = "";
|
||||
poolRequired.value = { id, name: p.name };
|
||||
}
|
||||
|
||||
async function confirmPool() {
|
||||
const req = poolRequired.value!;
|
||||
try {
|
||||
let pool = poolPick.value;
|
||||
if (pool === DEFAULT_PICK) {
|
||||
pool = await invoke<string>("create_reference_pool", {
|
||||
name: t("pools.referenceDefaultName"),
|
||||
dir: defaultDir.value,
|
||||
});
|
||||
}
|
||||
await invoke("assign_pool", { project: req.id, pool });
|
||||
poolRequired.value = null;
|
||||
await invoke("open_terminal", { project: req.id });
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
error.value = String(e);
|
||||
poolRequired.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Abbruch ist eine Entscheidung, kein Nichts: kein Terminal, und die Meldung
|
||||
// sagt, warum.
|
||||
function cancelPool() {
|
||||
error.value = t("projects.poolRequiredCancelled", { name: poolRequired.value!.name });
|
||||
poolRequired.value = null;
|
||||
}
|
||||
|
||||
let timer: number;
|
||||
onMounted(() => {
|
||||
let unlistenPool: UnlistenFn | undefined;
|
||||
onMounted(async () => {
|
||||
refresh();
|
||||
timer = window.setInterval(refresh, 3000);
|
||||
unlistenPool = await listen<string>("pool-required", (e) => askForPool(e.payload));
|
||||
});
|
||||
onUnmounted(() => {
|
||||
window.clearInterval(timer);
|
||||
unlistenPool?.();
|
||||
});
|
||||
onUnmounted(() => window.clearInterval(timer));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -892,6 +953,31 @@ onUnmounted(() => window.clearInterval(timer));
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="poolRequired" class="overlay">
|
||||
<div class="dialog">
|
||||
<h3>{{ $t("projects.poolRequiredTitle", { name: poolRequired.name }) }}</h3>
|
||||
<p>{{ $t("projects.poolRequiredHint") }}</p>
|
||||
<label class="field">
|
||||
{{ $t("projects.pool") }}
|
||||
<select v-model="poolPick">
|
||||
<option value="" disabled>{{ $t("projects.poolRequiredChoose") }}</option>
|
||||
<option v-for="pool in pools" :key="pool.id" :value="pool.id">
|
||||
{{ pool.name }}
|
||||
</option>
|
||||
<option v-if="defaultDir" :value="DEFAULT_PICK">
|
||||
{{ $t("projects.poolRequiredDefault", { dir: contractHome(defaultDir) }) }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<div class="actions">
|
||||
<button type="button" @click="cancelPool">{{ $t("projects.cancel") }}</button>
|
||||
<button class="primary" :disabled="!poolPick" @click="confirmPool">
|
||||
{{ $t("projects.start") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -55,6 +55,12 @@ const de = {
|
||||
noPoolAssigned: "kein Pool",
|
||||
needsPool: "Kein Pool zugeordnet",
|
||||
needsKey: "Pool hat keinen API-Key",
|
||||
poolRequiredTitle: "{name} hat keinen Pool",
|
||||
poolRequiredHint:
|
||||
"Jede Session läuft in einem Pool — welcher es sein soll, entscheidest du. Ohne Wahl startet das Projekt nicht.",
|
||||
poolRequiredChoose: "– bitte wählen –",
|
||||
poolRequiredDefault: "Standard-Verzeichnis ({dir})",
|
||||
poolRequiredCancelled: "{name} wurde nicht gestartet: kein Pool gewählt.",
|
||||
newProject: "+ Projekt",
|
||||
addFolder: "Projekt importieren …",
|
||||
addFolderTitle: "Bestehenden Ordner als Projekt aufnehmen",
|
||||
@@ -335,6 +341,12 @@ const en: typeof de = {
|
||||
noPoolAssigned: "no pool",
|
||||
needsPool: "No pool assigned",
|
||||
needsKey: "Pool has no API key",
|
||||
poolRequiredTitle: "{name} has no pool",
|
||||
poolRequiredHint:
|
||||
"Every session runs in a pool — which one is your call. Without a choice the project does not start.",
|
||||
poolRequiredChoose: "– please choose –",
|
||||
poolRequiredDefault: "Default directory ({dir})",
|
||||
poolRequiredCancelled: "{name} was not started: no pool chosen.",
|
||||
newProject: "+ Project",
|
||||
addFolder: "Import project …",
|
||||
addFolderTitle: "Add an existing folder as a project",
|
||||
|
||||
Reference in New Issue
Block a user