feat(settings): réglages Claude CLI (binaire + ~/.claude) & notif « disponible »

- Réglages → Claude CLI : override du chemin du binaire `claude` (effet à la
  prochaine session, fallback `which claude`), diagnostic de détection, et
  override de la racine ~/.claude (effet au redémarrage). Validateurs stricts.
- Push : notifie aussi sur le front busy→idle (session redevenue disponible).
- Renomme l'état affiché idle → « disponible » / « available » (web EN/FR + VS Code) ;
  l'enum SessionActivity du protocole reste inchangé.
This commit is contained in:
2026-06-24 10:15:29 +02:00
parent 2506dfb1f3
commit b5236b41c8
16 changed files with 519 additions and 27 deletions

View File

@@ -56,7 +56,7 @@ export default {
},
registryStatus: {
busy: 'busy',
idle: 'idle',
idle: 'available',
waiting: 'waiting',
},
dialog: {
@@ -283,7 +283,7 @@ export default {
live: 'Live',
waiting: 'Waiting',
busy: 'Busy',
idle: 'Idle',
idle: 'Available',
exited: 'Exited',
resumable: 'Resumable',
},
@@ -413,6 +413,20 @@ export default {
removeRoot: 'Remove',
scanInterval: 'Re-scan interval (minutes)',
scanIntervalHint: '0 disables periodic scanning. Changes apply on the next daemon restart.',
// Claude CLI
claudeCli: 'Claude CLI',
claudeCliHint: 'Where Arboretum finds the `claude` binary and its data. Handy when the daemon runs as a service with a minimal PATH that does not include ~/.local/bin.',
claudeBinStatusOk: 'Detected',
claudeBinStatusNotFound: 'Not found — sessions cannot start until this is resolved.',
claudeBinStatusBadPath: 'The configured path is not an executable file.',
claudeBinSourceConfigured: 'configured',
claudeBinSourcePath: 'PATH',
claudeBinPathLabel: 'Binary path override',
claudeBinPathHint: 'Absolute path to the `claude` executable. Leave empty to auto-detect via PATH. Takes effect on the next session.',
claudeBinPathPlaceholder: 'e.g. /home/you/.local/bin/claude',
claudeHomeLabel: 'Claude home directory',
claudeHomeHint: 'Override the ~/.claude location (transcripts & session registry). Applies on the next daemon restart.',
claudeHomePlaceholder: 'e.g. /home/you/.claude',
// Serveur (lecture seule)
server: 'Server',
serverHint: 'Set at startup via CLI flags — changing them requires restarting the daemon.',

View File

@@ -58,7 +58,7 @@ const fr: typeof en = {
},
registryStatus: {
busy: 'occupée',
idle: 'inactive',
idle: 'disponible',
waiting: 'en attente',
},
dialog: {
@@ -286,7 +286,7 @@ const fr: typeof en = {
live: 'Active',
waiting: 'En attente',
busy: 'Occupée',
idle: 'Au repos',
idle: 'Disponible',
exited: 'Terminée',
resumable: 'Reprenable',
},
@@ -416,6 +416,20 @@ const fr: typeof en = {
removeRoot: 'Retirer',
scanInterval: 'Intervalle de re-scan (minutes)',
scanIntervalHint: '0 désactive le scan périodique. Les changements prennent effet au prochain redémarrage du daemon.',
// Claude CLI
claudeCli: 'CLI Claude',
claudeCliHint: 'Où Arboretum trouve le binaire `claude` et ses données. Utile quand le daemon tourne en service avec un PATH minimal qui ninclut pas ~/.local/bin.',
claudeBinStatusOk: 'Détecté',
claudeBinStatusNotFound: 'Introuvable — impossible de lancer une session tant que ce nest pas résolu.',
claudeBinStatusBadPath: 'Le chemin configuré nest pas un fichier exécutable.',
claudeBinSourceConfigured: 'configuré',
claudeBinSourcePath: 'PATH',
claudeBinPathLabel: 'Chemin du binaire (override)',
claudeBinPathHint: 'Chemin absolu vers lexécutable `claude`. Laisser vide pour lauto-détection via le PATH. Prend effet à la prochaine session.',
claudeBinPathPlaceholder: 'ex. /home/vous/.local/bin/claude',
claudeHomeLabel: 'Répertoire ~/.claude',
claudeHomeHint: 'Surcharge lemplacement de ~/.claude (transcripts & registre des sessions). Prend effet au redémarrage du daemon.',
claudeHomePlaceholder: 'ex. /home/vous/.claude',
// Serveur (lecture seule)
server: 'Serveur',
serverHint: 'Définis au démarrage via des flags CLI — les modifier nécessite de redémarrer le daemon.',

View File

@@ -9,6 +9,8 @@ export const useSettingsStore = defineStore('settings', () => {
const server = ref<ServerInfo | null>(null);
const scanRoots = ref<string[]>([]);
const scanIntervalMin = ref(0);
const claudeBinPath = ref<string | null>(null);
const claudeHome = ref<string | null>(null);
const loaded = ref(false);
const saving = ref(false);
@@ -16,6 +18,8 @@ export const useSettingsStore = defineStore('settings', () => {
server.value = res.server;
scanRoots.value = res.settings.scanRoots;
scanIntervalMin.value = res.settings.scanIntervalMin;
claudeBinPath.value = res.settings.claudeBinPath;
claudeHome.value = res.settings.claudeHome;
loaded.value = true;
}
@@ -32,5 +36,5 @@ export const useSettingsStore = defineStore('settings', () => {
}
}
return { server, scanRoots, scanIntervalMin, loaded, saving, fetch, save };
return { server, scanRoots, scanIntervalMin, claudeBinPath, claudeHome, loaded, saving, fetch, save };
});

View File

@@ -137,6 +137,44 @@
</div>
</section>
<!-- Claude CLI -->
<section class="card flex flex-col gap-3">
<h2 class="flex items-center gap-2 text-sm font-semibold text-zinc-100">
<Terminal :size="16" /> {{ t('settings.claudeCli') }}
</h2>
<p class="text-xs text-zinc-500">{{ t('settings.claudeCliHint') }}</p>
<!-- Diagnostic de détection -->
<div v-if="settings.server" class="card-inset flex flex-wrap items-center gap-x-2 gap-y-1 text-xs">
<component :is="claudeBin.ok ? CheckCircle2 : AlertCircle" :size="15" :class="claudeBin.ok ? 'text-emerald-400' : 'text-rose-400'" />
<span :class="claudeBin.ok ? 'text-zinc-300' : 'text-rose-300'">
{{ claudeBin.ok ? t('settings.claudeBinStatusOk') : (claudeBin.source === 'configured' ? t('settings.claudeBinStatusBadPath') : t('settings.claudeBinStatusNotFound')) }}
</span>
<code v-if="claudeBin.path" class="min-w-0 truncate font-mono text-zinc-200" :title="claudeBin.path">{{ claudeBin.path }}</code>
<span v-if="claudeBin.source" class="badge bg-zinc-700/40 text-zinc-300">
{{ claudeBin.source === 'configured' ? t('settings.claudeBinSourceConfigured') : t('settings.claudeBinSourcePath') }}
</span>
</div>
<label class="flex flex-col gap-1">
<span class="text-xs text-zinc-400">{{ t('settings.claudeBinPathLabel') }}</span>
<input v-model.trim="claudeBinDraft" class="input font-mono" :placeholder="t('settings.claudeBinPathPlaceholder')" />
<span class="text-xs text-zinc-500">{{ t('settings.claudeBinPathHint') }}</span>
</label>
<label class="flex flex-col gap-1">
<span class="text-xs text-zinc-400">{{ t('settings.claudeHomeLabel') }}</span>
<input v-model.trim="claudeHomeDraft" class="input font-mono" :placeholder="t('settings.claudeHomePlaceholder')" />
<span class="text-xs text-zinc-500">{{ t('settings.claudeHomeHint') }}</span>
</label>
<div>
<BaseButton variant="primary" :loading="settings.saving" :disabled="!claudeDirty" @click="saveClaude">
{{ t('settings.save') }}
</BaseButton>
</div>
</section>
<!-- Sécurité & conformité -->
<section class="card flex flex-col gap-3">
<h2 class="flex items-center gap-2 text-sm font-semibold text-zinc-100">
@@ -202,7 +240,7 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { Bell, BellOff, Check, Coffee, Copy, Download, FolderOpen, KeyRound, Plus, RefreshCw, ScanSearch, Send, Server, Shield, SlidersHorizontal, Trash2, X } from '@lucide/vue';
import { AlertCircle, Bell, BellOff, Check, CheckCircle2, Coffee, Copy, Download, FolderOpen, KeyRound, Plus, RefreshCw, ScanSearch, Send, Server, Shield, SlidersHorizontal, Terminal, Trash2, X } from '@lucide/vue';
import type {
AuditLogEntry,
AuditLogsResponse,
@@ -328,6 +366,27 @@ async function saveDiscovery(): Promise<void> {
}
}
// ---- Claude CLI ----
const claudeBinDraft = ref('');
const claudeHomeDraft = ref('');
const claudeBin = computed(() => settings.server?.claudeBin ?? { path: null, source: null, ok: false });
const claudeDirty = computed(
() => claudeBinDraft.value !== (settings.claudeBinPath ?? '') || claudeHomeDraft.value !== (settings.claudeHome ?? ''),
);
function syncClaudeDraft(): void {
claudeBinDraft.value = settings.claudeBinPath ?? '';
claudeHomeDraft.value = settings.claudeHome ?? '';
}
async function saveClaude(): Promise<void> {
try {
await settings.save({ claudeBinPath: claudeBinDraft.value, claudeHome: claudeHomeDraft.value });
syncClaudeDraft();
toasts.success(t('settings.saved'));
} catch (e) {
toasts.error(e);
}
}
// ---- Sécurité & conformité (audit / RGPD) ----
const audit = ref<AuditLogEntry[]>([]);
const auditing = ref(false);
@@ -378,6 +437,7 @@ async function deleteMyData(): Promise<void> {
onMounted(async () => {
await Promise.all([loadTokens(), settings.loaded ? Promise.resolve() : settings.fetch()]);
syncDiscoveryDraft();
syncClaudeDraft();
void push.refresh();
});
</script>