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

@@ -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>