refactor: remove locale panel, improve provider validation and terminal buffer persistence
All checks were successful
Beta Release / beta (push) Successful in 47s

- Remove locale panel from config (language/keyboard already handled elsewhere)
- Add per-provider key validation status with auto-check on load
- Add missing tools section with AI-powered installation
- Improve reset confirmation with modal
- Persist terminal buffer to localStorage with auto-save
- Detect clear command to wipe saved buffer
- Remove AI tab concept (commands routed to active tab instead)
- Remove renderTick hacks, use proper message keys

💘 Generated with Crush

Assisted-by: MiniMax-M2.7 via Crush <crush@charm.land>
This commit is contained in:
Augustin
2026-04-24 13:49:12 +02:00
parent 167ab82978
commit 00118f0803
3 changed files with 200 additions and 211 deletions

View File

@@ -1,13 +1,11 @@
import { useState, useEffect, useCallback } from 'react'
import { User, Brain, RefreshCw, Globe, Wrench, Monitor } from 'lucide-react'
import { useI18n, LANGUAGES } from '../i18n'
import { getLayoutList } from '../i18n/keyboards'
import { User, Brain, RefreshCw, Wrench, Monitor, AlertTriangle, X } from 'lucide-react'
import { useI18n } from '../i18n'
const PANELS = [
{ id: 'profile', icon: User },
{ id: 'providers', icon: Brain },
{ id: 'updates', icon: RefreshCw },
{ id: 'locale', icon: Globe },
{ id: 'skills', icon: Wrench },
{ id: 'system', icon: Monitor },
]
@@ -29,8 +27,6 @@ export default function Config({ api }) {
const [toast, setToast] = useState(null)
const layouts = getLayoutList()
const loadData = useCallback(() => {
api.getConfig().then(d => {
setConfig(d)
@@ -168,13 +164,6 @@ export default function Config({ api }) {
t={t}
/>
)}
{activePanel === 'locale' && (
<PanelLocale
language={language} keyboard={keyboard} layouts={layouts}
api={api}
t={t}
/>
)}
{activePanel === 'skills' && (
<PanelSkills skillList={skillList} t={t} />
)}
@@ -320,21 +309,36 @@ function getFieldLabel(key, t) {
function PanelProviders({ providers, editProvider, providerForm, setProviderForm, setEditProvider, openProviderEdit, handleSaveProvider, api, loadData, t }) {
const [validating, setValidating] = useState(null)
const [validationStatus, setValidationStatus] = useState(null)
const [keyStatus, setKeyStatus] = useState({})
useEffect(() => {
providers.forEach(p => {
if (p.apiKey && !keyStatus[p.name]) {
validateKey(p)
} else if (!p.apiKey) {
setKeyStatus(prev => ({ ...prev, [p.name]: { valid: false, checked: true, error: 'Aucune clé' } }))
}
})
}, [providers])
const validateKey = async (p) => {
setValidating(p.name)
try {
await api.validateProvider({ name: p.name, api_key: p.apiKey, model: p.model, base_url: p.baseURL || '' })
setKeyStatus(prev => ({ ...prev, [p.name]: { valid: true, checked: true } }))
} catch (err) {
setKeyStatus(prev => ({ ...prev, [p.name]: { valid: false, checked: true, error: err.message || 'Clé invalide' } }))
}
setValidating(null)
}
const handleValidate = async (name, apiKey, model, baseUrl) => {
setValidating(name)
setValidationStatus(null)
try {
await api.validateProvider({ name, api_key: apiKey, model, base_url: baseUrl })
setValidationStatus({ provider: name, valid: true })
setKeyStatus(prev => ({ ...prev, [name]: { valid: true, checked: true } }))
} catch (err) {
const msg = err.message || ''
if (msg.includes('invalid_api_key')) {
setValidationStatus({ provider: name, valid: false, error: t('config.keyInvalid') })
} else {
setValidationStatus({ provider: name, valid: false, error: `${t('config.connectionFailed')}: ${msg}` })
}
setKeyStatus(prev => ({ ...prev, [name]: { valid: false, checked: true, error: err.message || 'Clé invalide' } }))
}
setValidating(null)
}
@@ -345,8 +349,8 @@ function PanelProviders({ providers, editProvider, providerForm, setProviderForm
<div className="config-providers-list">
{displayed.map((p, i) => {
const isEditing = editProvider === p.name
const isValidationTarget = validationStatus?.provider === p.name
const currentModel = providerForm[p.name]?.model || p.model
const status = keyStatus[p.name]
return (
<div key={i} className="config-card provider-card-v2">
@@ -354,8 +358,8 @@ function PanelProviders({ providers, editProvider, providerForm, setProviderForm
<div className="provider-card-identity">
<span className="provider-card-name">{p.name.toUpperCase()}</span>
{p.active && <span className="badge ok" style={{ marginLeft: 6 }}>active</span>}
{isValidationTarget && validationStatus?.valid && <span className="badge ok">{t('config.keyValid')}</span>}
{isValidationTarget && !validationStatus?.valid && <span className="badge error">{validationStatus?.error}</span>}
{status?.checked && status?.valid && <span className="badge ok"> {t('config.keyValid')}</span>}
{status?.checked && !status?.valid && <span className="badge error"> {status.error || t('config.keyInvalid')}</span>}
</div>
</div>
@@ -402,7 +406,14 @@ function PanelProviders({ providers, editProvider, providerForm, setProviderForm
)
}
function PanelUpdates({ updates, checking, updating, needsUpdateCount, installedCount, missingCount, handleCheckUpdates, handleUpdateTool, handleUpdateAll, t }) {
function PanelUpdates({ updates, tools, checking, updating, needsUpdateCount, installedCount, missingCount, handleCheckUpdates, handleUpdateTool, handleUpdateAll, t }) {
const handleInstallTool = (tool) => {
window.dispatchEvent(new CustomEvent('navigate-to-shell', {}))
window.dispatchEvent(new CustomEvent('ask-ai-terminal', { detail: { message: `Installe l'outil ${tool} sur mon système. Vérifie d'abord s'il est déjà installé, puis installe-le si nécessaire avec les commandes appropriées.` } }))
}
const missingTools = tools.filter(t => !t.installed)
return (
<>
<div className="config-card">
@@ -425,6 +436,30 @@ function PanelUpdates({ updates, checking, updating, needsUpdateCount, installed
</div>
</div>
{missingTools.length > 0 && (
<>
<div className="section-title" style={{ marginTop: 12, marginBottom: 4 }}>{t('config.missing') || 'Modules manquants'}</div>
<div className="config-update-list">
{missingTools.map((tool, i) => (
<div key={`miss-${i}`} className="config-update-row">
<div className="config-update-info">
<span className="config-update-name">{tool.name}</span>
<span className="config-update-versions">
<span style={{ color: 'var(--danger)' }}>{t('config.notInstalled') || 'Non installé'}</span>
</span>
</div>
<button
className="sm primary"
onClick={() => handleInstallTool(tool.name)}
>
{t('config.install') || 'Installer'}
</button>
</div>
))}
</div>
</>
)}
{updates.length === 0 ? (
<div className="config-card">
<div className="empty-state">{t('config.noUpdates')}</div>
@@ -460,98 +495,7 @@ function PanelUpdates({ updates, checking, updating, needsUpdateCount, installed
)
}
function PanelLocale({ language, keyboard, layouts, api, t }) {
const { setLanguage, setKeyboard } = useI18n()
const [editLocale, setEditLocale] = useState(false)
const [draftLang, setDraftLang] = useState(language)
const [draftKbd, setDraftKbd] = useState(keyboard)
const [saving, setSaving] = useState(false)
const [toast, setToast] = useState(null)
const showToast = (msg) => {
setToast(msg)
setTimeout(() => setToast(null), 2500)
}
const handleSave = async () => {
setSaving(true)
try {
await api.savePreferences({ language: draftLang, keyboard_layout: draftKbd })
setLanguage(draftLang)
setKeyboard(draftKbd)
setEditLocale(false)
showToast(t('config.saved'))
} catch (err) {
showToast(`${t('config.error')}: ${err.message}`)
}
setSaving(false)
}
const currentLang = LANGUAGES.find(l => l.id === language)
const currentKbd = layouts.find(l => l.id === keyboard)
return (
<div className="config-profile-center">
{toast && <div className="config-toast">{toast}</div>}
<div className="config-card">
<div className="config-card-row">
<span className="config-card-label">{t('config.language')}</span>
<span className="config-card-value">{currentLang?.name || language}</span>
</div>
<div className="config-card-row">
<span className="config-card-label">{t('config.keyboardLayout')}</span>
<span className="config-card-value">{currentKbd?.name || keyboard}</span>
</div>
</div>
{editLocale && (
<div className="config-card">
<div className="config-card-group">
<span className="config-card-group-label">{t('config.language')}</span>
<div className="chip-row">
{LANGUAGES.map(lang => (
<div
key={lang.id}
className={`chip ${draftLang === lang.id ? 'active' : ''}`}
onClick={() => setDraftLang(lang.id)}
>
{lang.name}
</div>
))}
</div>
</div>
<div className="config-card-group">
<span className="config-card-group-label">{t('config.keyboardLayout')}</span>
<div className="chip-row">
{layouts.map(l => (
<div
key={l.id}
className={`chip ${draftKbd === l.id ? 'active' : ''}`}
onClick={() => setDraftKbd(l.id)}
>
{l.name}
</div>
))}
</div>
</div>
</div>
)}
<div className="config-card">
<div className="config-card-actions" style={{ justifyContent: 'center' }}>
{editLocale ? (
<>
<button className="primary sm" onClick={handleSave} disabled={saving}>
{saving ? t('config.saving') : t('config.save')}
</button>
<button className="ghost sm" onClick={() => setEditLocale(false)}>{t('config.cancel')}</button>
</>
) : (
<button className="primary sm" onClick={() => { setDraftLang(language); setDraftKbd(keyboard); setEditLocale(true) }}>{t('config.editProfile')}</button>
)}
</div>
</div>
</div>
)
}
function PanelSkills({ skillList, t }) {
const [selected, setSelected] = useState(null)
@@ -634,7 +578,7 @@ function PanelSkills({ skillList, t }) {
}
function PanelSystem({ api, t }) {
const [resetConfirm, setResetConfirm] = useState(false)
const [showResetModal, setShowResetModal] = useState(false)
const [toast, setToast] = useState(null)
const showToast = (msg) => {
@@ -645,7 +589,7 @@ function PanelSystem({ api, t }) {
const handleReset = async () => {
try {
await api.resetConfig()
setResetConfirm(false)
setShowResetModal(false)
showToast(t('config.resetDone'))
setTimeout(() => window.location.reload(), 1500)
} catch (err) {
@@ -653,49 +597,66 @@ function PanelSystem({ api, t }) {
}
}
const handleApplyStarship = async () => {
try {
await api.applyStarshipTheme('charm')
showToast(t('config.starshipApplied'))
} catch (err) {
showToast(`${t('config.error')}: ${err.message}`)
}
const handleApplyStarship = () => {
window.dispatchEvent(new CustomEvent('navigate-to-shell', {}))
window.dispatchEvent(new CustomEvent('ask-ai-terminal', { detail: { message: `Vérifie si starship est installé sur le système. S'il ne l'est pas, installe-le (avec curl ou le gestionnaire de paquets). Ensuite, applique la configuration du thème "charm" pour starship. Assure-toi que starship est bien initialisé dans le shell de l'utilisateur.` } }))
}
return (
<>
{toast && <div className="config-toast">{toast}</div>}
<div className="section-title" style={{ marginBottom: 8 }}>Configuration Système</div>
<div className="config-card">
<div className="config-card-row" style={{ marginBottom: 16 }}>
<span className="config-card-label" style={{ fontWeight: 600 }}>{t('config.applyStarship')}</span>
</div>
<div style={{ fontSize: 12, color: 'var(--muted)', marginBottom: 12 }}>
{t('config.starshipApplied')}
<div style={{ fontSize: 12, color: 'var(--text-tertiary)', marginBottom: 12 }}>
Vérifie l'installation de starship et configure le thème charm via l'IA.
</div>
<button className="sm primary" onClick={handleApplyStarship}>
{t('config.applyStarship')}
</button>
</div>
<div className="config-card" style={{ marginTop: 12 }}>
<div className="section-title" style={{ marginTop: 20, marginBottom: 8, color: 'var(--danger)' }}>
<AlertTriangle size={14} style={{ verticalAlign: 'middle', marginRight: 6 }} />
Zone Rouge
</div>
<div className="config-card" style={{ borderColor: 'var(--danger)', borderWidth: 1, borderStyle: 'solid' }}>
<div className="config-card-row" style={{ marginBottom: 16 }}>
<span className="config-card-label" style={{ fontWeight: 600 }}>{t('config.resetConfig')}</span>
<span className="config-card-label" style={{ fontWeight: 600, color: 'var(--danger)' }}>{t('config.resetConfig')}</span>
</div>
{resetConfirm ? (
<div>
<div style={{ fontSize: 12, color: 'var(--warning)', marginBottom: 12 }}>
{t('config.resetConfirm')}
<div style={{ fontSize: 12, color: 'var(--text-tertiary)', marginBottom: 12 }}>
Cette action supprimera toute votre configuration et relancera l'application.
</div>
<button className="sm ghost danger" onClick={() => setShowResetModal(true)}>
{t('config.resetConfig')}
</button>
</div>
{showResetModal && (
<div className="shell-modal-overlay" onClick={() => setShowResetModal(false)}>
<div className="shell-modal" onClick={e => e.stopPropagation()}>
<div className="shell-modal-header" style={{ color: 'var(--danger)' }}>
<AlertTriangle size={16} style={{ verticalAlign: 'middle', marginRight: 8 }} />
{t('config.resetConfig')}
</div>
<div style={{ display: 'flex', gap: 8 }}>
<button className="sm" onClick={() => setResetConfirm(false)}>{t('config.cancel')}</button>
<button className="sm danger" onClick={handleReset}>{t('config.resetConfig')}</button>
<div className="shell-modal-body">
<p style={{ color: 'var(--warning)', fontSize: 13, marginBottom: 12 }}>
{t('config.resetConfirm')}
</p>
<p style={{ color: 'var(--text-tertiary)', fontSize: 12 }}>
Cette action est irréversible. Toute votre configuration (profil, clés API, préférences) sera supprimée.
</p>
</div>
<div className="shell-modal-footer">
<button className="ghost" onClick={() => setShowResetModal(false)}>{t('config.cancel')}</button>
<button className="danger" onClick={handleReset}>{t('config.resetConfig')}</button>
</div>
</div>
) : (
<button className="sm ghost danger" onClick={() => setResetConfirm(true)}>
{t('config.resetConfig')}
</button>
)}
</div>
</div>
)}
</>
)
}