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)
|
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,
|
/// Tray-Klick auf ein Projekt: läuft es, kommt das Terminal-Fenster nach vorn,
|
||||||
/// sonst startet es.
|
/// sonst startet es.
|
||||||
fn start_or_focus(app: &tauri::AppHandle, project: &str) {
|
fn start_or_focus(app: &tauri::AppHandle, project: &str) {
|
||||||
@@ -298,10 +310,14 @@ fn start_or_focus(app: &tauri::AppHandle, project: &str) {
|
|||||||
Some(pid) => crate::platform::focus_terminal(*pid),
|
Some(pid) => crate::platform::focus_terminal(*pid),
|
||||||
None => {
|
None => {
|
||||||
if let Err(e) = terminal::open_terminal(app.clone(), project.to_string()) {
|
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}");
|
eprintln!("{project} starten: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tray-/Popup-Klick: Projekt starten oder das laufende Terminal fokussieren.
|
/// Tray-/Popup-Klick: Projekt starten oder das laufende Terminal fokussieren.
|
||||||
|
|||||||
@@ -205,9 +205,20 @@ pub(crate) fn verify_project_dir_in(paths: &Paths, project: &str) -> Result<Path
|
|||||||
Ok(dir)
|
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
|
/// Pool-Config-Verzeichnis eines Projekts — wird dem Terminal als
|
||||||
/// CLAUDE_CONFIG_DIR mitgegeben.
|
/// 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
|
/// 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
|
/// tut: Die Migration übernimmt Pool-Einträge aus der alten, versionierten
|
||||||
/// ai-control.json eines geklonten Repos. Ungeprüft bestimmte ein `../…`
|
/// ai-control.json eines geklonten Repos. Ungeprüft bestimmte ein `../…`
|
||||||
@@ -223,7 +234,7 @@ pub(crate) fn project_pool_dir_in(
|
|||||||
project: &str,
|
project: &str,
|
||||||
) -> Result<Option<PathBuf>, String> {
|
) -> Result<Option<PathBuf>, String> {
|
||||||
let Some(pool) = project_pool(paths, project)? else {
|
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}"))?;
|
check_name(&pool).map_err(|_| format!("ungültiger Pool in der Registry: {pool}"))?;
|
||||||
let dir = crate::domain::pool::pool_config_dir(paths, &pool)?;
|
let dir = crate::domain::pool::pool_config_dir(paths, &pool)?;
|
||||||
@@ -798,6 +809,18 @@ mod tests {
|
|||||||
assert!(err.contains("ungültiger Pool"));
|
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
|
/// Das Setzen der Terminal-Config kommt aus der Oberfläche und kennt nur
|
||||||
/// theme/icon/title — unbekannte Keys müssen trotzdem stehen bleiben.
|
/// theme/icon/title — unbekannte Keys müssen trotzdem stehen bleiben.
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ pub fn open_terminal(app: AppHandle, project: String) -> Result<(), String> {
|
|||||||
if !dir.is_dir() {
|
if !dir.is_dir() {
|
||||||
return Err(format!("Projektordner fehlt: {}", dir.display()));
|
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();
|
let bundle_id = app.config().identifier.clone();
|
||||||
app
|
app
|
||||||
.run_on_main_thread(move || crate::platform::yield_activation(&bundle_id))
|
.run_on_main_thread(move || crate::platform::yield_activation(&bundle_id))
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, onUnmounted, ref } from "vue";
|
import { onMounted, onUnmounted, ref } from "vue";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
|
||||||
import { open } from "@tauri-apps/plugin-dialog";
|
import { open } from "@tauri-apps/plugin-dialog";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
interface Project {
|
interface Project {
|
||||||
id: string;
|
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;
|
let timer: number;
|
||||||
onMounted(() => {
|
let unlistenPool: UnlistenFn | undefined;
|
||||||
|
onMounted(async () => {
|
||||||
refresh();
|
refresh();
|
||||||
timer = window.setInterval(refresh, 3000);
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -892,6 +953,31 @@ onUnmounted(() => window.clearInterval(timer));
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -55,6 +55,12 @@ const de = {
|
|||||||
noPoolAssigned: "kein Pool",
|
noPoolAssigned: "kein Pool",
|
||||||
needsPool: "Kein Pool zugeordnet",
|
needsPool: "Kein Pool zugeordnet",
|
||||||
needsKey: "Pool hat keinen API-Key",
|
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",
|
newProject: "+ Projekt",
|
||||||
addFolder: "Projekt importieren …",
|
addFolder: "Projekt importieren …",
|
||||||
addFolderTitle: "Bestehenden Ordner als Projekt aufnehmen",
|
addFolderTitle: "Bestehenden Ordner als Projekt aufnehmen",
|
||||||
@@ -335,6 +341,12 @@ const en: typeof de = {
|
|||||||
noPoolAssigned: "no pool",
|
noPoolAssigned: "no pool",
|
||||||
needsPool: "No pool assigned",
|
needsPool: "No pool assigned",
|
||||||
needsKey: "Pool has no API key",
|
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",
|
newProject: "+ Project",
|
||||||
addFolder: "Import project …",
|
addFolder: "Import project …",
|
||||||
addFolderTitle: "Add an existing folder as a project",
|
addFolderTitle: "Add an existing folder as a project",
|
||||||
|
|||||||
Reference in New Issue
Block a user