diff --git a/.gitea/workflows/ci-main.yml b/.gitea/workflows/ci-main.yml
index 9421d47..b32cd79 100644
--- a/.gitea/workflows/ci-main.yml
+++ b/.gitea/workflows/ci-main.yml
@@ -170,7 +170,7 @@ jobs:
- name: Commit changelog
env:
- GITEA_TOKEN: ${{ secrets.GITEATOKEN }}
+ GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
git config user.name "CI Bot"
git config user.email "ci@legion-muyue.fr"
@@ -181,16 +181,28 @@ jobs:
- name: Create release
env:
- GITEA_TOKEN: ${{ secrets.GITEATOKEN }}
+ GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
if [ -z "$GITEA_TOKEN" ]; then
- echo "Warning: GITEATOKEN not set, skipping release"
- exit 0
+ echo "Error: GITEA_TOKEN secret is not set"
+ exit 1
fi
VERSION=${{ steps.version.outputs.version }}
API="${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases"
+ echo "=== Debug ==="
+ echo "GITEA_TOKEN length: ${#GITEA_TOKEN}"
+ echo "API endpoint: ${API}"
+ echo "Creating release ${VERSION} at ${API}"
+
+ EXISTING_ID=$(curl -s -H "Authorization: token ${GITEA_TOKEN}" "${API}/tags/${VERSION}" | grep -o '"id":[0-9]*' | head -1 | grep -o '[0-9]*')
+ if [ -n "$EXISTING_ID" ]; then
+ echo "Release ${VERSION} already exists (ID: ${EXISTING_ID}), deleting..."
+ curl -s -X DELETE -H "Authorization: token ${GITEA_TOKEN}" "${API}/${EXISTING_ID}"
+ echo "Deleted existing release"
+ fi
+
BODY=$(cat /tmp/stable_changelog.md)
- RESPONSE=$(curl -s -X POST "${API}" \
+ RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "${API}" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d "{
@@ -201,10 +213,13 @@ jobs:
\"draft\":false,
\"prerelease\":false
}")
- RELEASE_ID=$(echo "$RESPONSE" | grep -o '"id":[0-9]*' | head -1 | grep -o '[0-9]*')
+ HTTP_CODE=$(echo "$RESPONSE" | tail -1)
+ RESPONSE_BODY=$(echo "$RESPONSE" | sed '$d')
+ echo "HTTP Status: ${HTTP_CODE}"
+ echo "Response: ${RESPONSE_BODY}"
+ RELEASE_ID=$(echo "$RESPONSE_BODY" | grep -o '"id":[0-9]*' | head -1 | grep -o '[0-9]*')
if [ -z "$RELEASE_ID" ]; then
- echo "Failed to create release:"
- echo "$RESPONSE"
+ echo "Failed to create release"
exit 1
fi
echo "Release ID: ${RELEASE_ID}"
@@ -212,8 +227,12 @@ jobs:
for file in dist/*.tar.gz dist/*.zip dist/checksums.txt; do
filename=$(basename "$file")
echo "Uploading ${filename}..."
- curl -s -X POST "${UPLOAD_URL}" \
+ UPLOAD_RESP=$(curl -s -w "\n%{http_code}" -X POST "${UPLOAD_URL}" \
-H "Authorization: token ${GITEA_TOKEN}" \
- -F "attachment=@${file};filename=${filename}" > /dev/null
+ -F "attachment=@${file};filename=${filename}")
+ UPLOAD_CODE=$(echo "$UPLOAD_RESP" | tail -1)
+ if [ "$UPLOAD_CODE" != "201" ]; then
+ echo "Upload failed with status ${UPLOAD_CODE}"
+ fi
done
echo "Stable release ${VERSION} published!"
diff --git a/internal/api/handlers_info.go b/internal/api/handlers_info.go
index 4e3190c..f031df0 100644
--- a/internal/api/handlers_info.go
+++ b/internal/api/handlers_info.go
@@ -477,9 +477,46 @@ func (s *Server) handleProvidersQuota(w http.ResponseWriter, r *http.Request) {
}
}
case "zai":
- // Z.AI (GLM) est utilisé uniquement via Crush, pas de quota check externe
- q.Healthy = true
- q.Data = map[string]interface{}{"note": "crush-only"}
+ if p.APIKey == "" {
+ q.Error = "no API key"
+ results = append(results, q)
+ continue
+ }
+ req, _ := http.NewRequest("GET", "https://api.z.ai/api/monitor/usage/quota/limit", nil)
+ req.Header.Set("Authorization", "Bearer "+p.APIKey)
+ req.Header.Set("Accept", "application/json")
+ resp, err := client.Do(req)
+ if err != nil {
+ q.Error = err.Error()
+ } else {
+ body, _ := io.ReadAll(resp.Body)
+ resp.Body.Close()
+ var data map[string]interface{}
+ if json.Unmarshal(body, &data) == nil {
+ if d, ok := data["data"].(map[string]interface{}); ok {
+ if limits, ok := d["limits"].([]interface{}); ok {
+ timeLimit := map[string]interface{}{}
+ for _, l := range limits {
+ if lm, ok := l.(map[string]interface{}); ok && lm["type"] == "TIME_LIMIT" {
+ usage, _ := lm["usage"].(float64)
+ remaining, _ := lm["remaining"].(float64)
+ total := usage + remaining
+ timeLimit = map[string]interface{}{
+ "model": "Z.AI",
+ "used": usage,
+ "total": total,
+ "remaining": remaining,
+ }
+ }
+ }
+ if len(timeLimit) > 0 {
+ q.Data = map[string]interface{}{"models": []map[string]interface{}{timeLimit}}
+ q.Healthy = true
+ }
+ }
+ }
+ }
+ }
case "claude", "anthropic":
// Claude Code n'a pas d'API externe, vérifier l'installation
claudePath := "/usr/bin/claude"
diff --git a/internal/api/handlers_shell_chat.go b/internal/api/handlers_shell_chat.go
index 2ac14b3..630ca24 100644
--- a/internal/api/handlers_shell_chat.go
+++ b/internal/api/handlers_shell_chat.go
@@ -277,3 +277,16 @@ Sois concret et technique. Le rapport sera utilisé comme contexte pour un assis
"analysis": result,
})
}
+
+func (s *Server) handleShellAnalysisGet(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "GET" {
+ writeError(w, "GET only", http.StatusMethodNotAllowed)
+ return
+ }
+ analysis := LoadSystemAnalysis()
+ if analysis == "" {
+ writeJSON(w, map[string]interface{}{"analysis": nil})
+ return
+ }
+ writeJSON(w, map[string]interface{}{"analysis": analysis})
+}
diff --git a/internal/api/server.go b/internal/api/server.go
index 3ec9b2e..4b89dd7 100644
--- a/internal/api/server.go
+++ b/internal/api/server.go
@@ -94,6 +94,7 @@ func (s *Server) routes() {
s.mux.HandleFunc("/api/shell/chat/history", s.handleShellChatHistory)
s.mux.HandleFunc("/api/shell/chat/clear", s.handleShellChatClear)
s.mux.HandleFunc("/api/shell/analyze", s.handleShellAnalyze)
+ s.mux.HandleFunc("/api/shell/analysis", s.handleShellAnalysisGet)
s.mux.HandleFunc("/api/workflow", s.handleWorkflowCreate)
s.mux.HandleFunc("/api/workflow/list", s.handleWorkflowList)
s.mux.HandleFunc("/api/workflow/", s.handleWorkflowGet)
diff --git a/internal/version/version.go b/internal/version/version.go
index e5377aa..8ac3608 100644
--- a/internal/version/version.go
+++ b/internal/version/version.go
@@ -7,7 +7,7 @@ import (
const (
Name = "muyue"
- Version = "0.3.3"
+ Version = "0.3.4"
Author = "La Légion de Muyue"
)
diff --git a/web/src/api/client.js b/web/src/api/client.js
index 4bd9be6..0946736 100644
--- a/web/src/api/client.js
+++ b/web/src/api/client.js
@@ -60,6 +60,7 @@ const api = {
getShellChatHistory: () => request('/shell/chat/history'),
clearShellChat: () => request('/shell/chat/clear', { method: 'POST' }),
analyzeSystem: () => request('/shell/analyze', { method: 'POST' }),
+ getShellAnalysis: () => request('/shell/analysis'),
sendChat: (message, stream = true, onChunk, signal) => {
if (!stream) {
return request('/chat', { method: 'POST', body: JSON.stringify({ message, stream: false }) })
diff --git a/web/src/components/App.jsx b/web/src/components/App.jsx
index 332df5f..1a681c6 100644
--- a/web/src/components/App.jsx
+++ b/web/src/components/App.jsx
@@ -76,6 +76,12 @@ export default function App() {
const switchTab = useCallback((tabId) => setActiveTab(tabId), [])
+ useEffect(() => {
+ const handler = () => setActiveTab('shell')
+ window.addEventListener('navigate-to-shell', handler)
+ return () => window.removeEventListener('navigate-to-shell', handler)
+ }, [])
+
const hasUpdates = updates.some(u => u.needsUpdate)
const installed = tools.filter(tool => tool.installed).length
diff --git a/web/src/components/Config.jsx b/web/src/components/Config.jsx
index 9611a97..f96bc2c 100644
--- a/web/src/components/Config.jsx
+++ b/web/src/components/Config.jsx
@@ -65,28 +65,15 @@ export default function Config({ api }) {
setChecking(false)
}
- const handleUpdateTool = async (tool) => {
- setUpdating(tool)
- try {
- await api.runUpdate(tool)
- await handleCheckUpdates()
- showToast(`${tool} ✓`)
- } catch (err) {
- showToast(`${t('config.error')}: ${err.message}`)
- }
- setUpdating(null)
+ const handleUpdateTool = (tool) => {
+ window.dispatchEvent(new CustomEvent('navigate-to-shell', {}))
+ window.dispatchEvent(new CustomEvent('ask-ai-terminal', { detail: { message: `Met à jour l'outil ${tool} sur mon système. Exécute les commandes nécessaires.` } }))
}
- const handleUpdateAll = async () => {
- setUpdating('__all__')
- try {
- await api.runUpdate('')
- await handleCheckUpdates()
- showToast(t('config.saved'))
- } catch (err) {
- showToast(`${t('config.error')}: ${err.message}`)
- }
- setUpdating(null)
+ const handleUpdateAll = () => {
+ const toUpdate = updates.filter(u => u.needsUpdate).map(u => u.tool)
+ window.dispatchEvent(new CustomEvent('navigate-to-shell', {}))
+ window.dispatchEvent(new CustomEvent('ask-ai-terminal', { detail: { message: `Met à jour tous les outils suivants sur mon système : ${toUpdate.join(', ')}. Exécute les commandes nécessaires une par une.` } }))
}
const handleSaveProfile = async () => {
@@ -403,20 +390,9 @@ function PanelProviders({ providers, editProvider, providerForm, setProviderForm
)}
-
-
{t('config.model')}
-
{
- setProviderForm(prev => ({
- ...prev,
- [p.name]: { ...(prev[p.name] || {}), model: e.target.value },
- }))
- setEditProvider(p.name)
- }}
- placeholder="model-name"
- />
+
+ {t('config.model')}
+ {p.model || '—'}
diff --git a/web/src/components/Dashboard.jsx b/web/src/components/Dashboard.jsx
index 6342766..e74080b 100644
--- a/web/src/components/Dashboard.jsx
+++ b/web/src/components/Dashboard.jsx
@@ -43,6 +43,7 @@ export default function Dashboard({ api, refreshRef }) {
const [recentCmds, setRecentCmds] = useState([])
const [processes, setProcesses] = useState([])
const [metrics, setMetrics] = useState(null)
+ const [copiedIdx, setCopiedIdx] = useState(-1)
const cpuRef = useRef([])
const memRef = useRef([])
const netRxRef = useRef([])
@@ -158,10 +159,19 @@ export default function Dashboard({ api, refreshRef }) {
{minimax.error || 'no data'}
)}
- {zai && (
+ {zai && zai.data?.models?.map((m, i) => (
+
+
{String(m.model)}
+
+
{m.used}/{m.total}
+
+ ))}
+ {zai && !zai.data?.models?.length && (
Z.AI
- {zai.healthy ? '✓ active' : zai.error || '—'}
+ {zai.error || 'no data'}
)}
{!minimax && !zai && No providers}
@@ -193,8 +203,8 @@ export default function Dashboard({ api, refreshRef }) {
{topCmds.length > 0 && (
{topCmds.map((c, i) => (
-
navigator.clipboard.writeText(c.cmd)} title="Copier">
-
{c.cmd}
+
{ navigator.clipboard.writeText(c.cmd); setCopiedIdx(i); setTimeout(() => setCopiedIdx(-1), 1200); }}>
+ {copiedIdx === i ? '✓ Copié' : c.cmd}
{c.count}×
))}
diff --git a/web/src/components/Shell.jsx b/web/src/components/Shell.jsx
index 230f573..dd8a6f5 100644
--- a/web/src/components/Shell.jsx
+++ b/web/src/components/Shell.jsx
@@ -1,13 +1,62 @@
-import { useState, useRef, useEffect, useCallback } from 'react'
+import { useState, useRef, useEffect, useCallback, useMemo } from 'react'
import { Terminal as XTerm } from '@xterm/xterm'
import { FitAddon } from '@xterm/addon-fit'
import { WebLinksAddon } from '@xterm/addon-web-links'
-import { Plus, X, Monitor, Globe, ChevronDown, Pencil, Trash2, Search, Copy, Send } from 'lucide-react'
+import { Plus, X, Monitor, Globe, ChevronDown, Pencil, Trash2, Search, Copy, Send, Eye, Bot } from 'lucide-react'
import '@xterm/xterm/css/xterm.css'
import { useI18n } from '../i18n'
+const AI_TAB_ID = 0
const MAX_TABS = 7
const SHELL_MAX_TOKENS = 100000
+const TABS_STORAGE_KEY = 'muyue_shell_tabs'
+
+function renderContent(text) {
+ const parts = []
+ const codeBlockRegex = /(```[\s\S]*?```)/g
+ let match
+ let lastIndex = 0
+ while ((match = codeBlockRegex.exec(text)) !== null) {
+ if (match.index > lastIndex) {
+ parts.push({ type: 'text', content: text.slice(lastIndex, match.index) })
+ }
+ const full = match[1]
+ const firstNewline = full.indexOf('\n')
+ const lang = firstNewline > -1 ? full.slice(3, firstNewline).trim() : ''
+ const code = firstNewline > -1 ? full.slice(firstNewline + 1, -3) : full.slice(3, -3)
+ parts.push({ type: 'code', lang, content: code })
+ lastIndex = match.index + full.length
+ }
+ if (lastIndex < text.length) {
+ parts.push({ type: 'text', content: text.slice(lastIndex) })
+ }
+ return parts
+}
+
+function formatText(text) {
+ let html = text
+ .replace(/&/g, '&').replace(//g, '>')
+
+ html = html
+ .replace(/\*\*(.+?)\*\*/g, '
$1')
+ .replace(/`([^`]+)`/g, '
$1')
+ .replace(/^### (.+)$/gm, '
$1
')
+ .replace(/^## (.+)$/gm, '
$1
')
+ .replace(/^# (.+)$/gm, '
$1
')
+ .replace(/^\s*[-*] (.+)$/gm, '
• $1
')
+ .replace(/^\s*(\d+)[.)] (.+)$/gm, '
$1 $2
')
+ .replace(/\n/g, '
')
+
+ html = html
+ .replace(/
\s*
/g, '
')
+ .replace(/
\s*(
{
+ try {
+ const raw = localStorage.getItem(TABS_STORAGE_KEY)
+ if (raw) {
+ const parsed = JSON.parse(raw)
+ if (Array.isArray(parsed) && parsed.length > 0) {
+ return parsed.map(t => ({ ...t, connected: false }))
+ }
+ }
+ } catch {}
+ return null
+ })()
+
+ const [tabs, setTabs] = useState(savedTabs || [
+ { id: AI_TAB_ID, name: 'AI Terminal', type: 'ai', shell: '', connected: false, ai: true },
{ id: 1, name: 'Local Shell', type: 'local', shell: '', connected: false },
])
- const [activeTab, setActiveTab] = useState(1)
+ const [activeTab, setActiveTab] = useState(() => {
+ if (savedTabs) {
+ const aiTab = savedTabs.find(t => t.ai)
+ return aiTab ? aiTab.id : savedTabs[0].id
+ }
+ return AI_TAB_ID
+ })
const [sshConnections, setSshConnections] = useState([])
const [systemTerminals, setSystemTerminals] = useState([])
const [showMenu, setShowMenu] = useState(false)
@@ -160,6 +230,8 @@ export default function Shell({ api }) {
theme: 'default',
})
+ useEffect(() => { settingsRef.current = terminalSettings }, [terminalSettings])
+
const [sshForm, setSshForm] = useState({
name: '', host: '', port: 22, user: '', key_path: '',
})
@@ -170,6 +242,9 @@ export default function Shell({ api }) {
const [aiTokens, setAiTokens] = useState(0)
const [aiAtLimit, setAiAtLimit] = useState(false)
const [analyzing, setAnalyzing] = useState(false)
+ const [showAnalysis, setShowAnalysis] = useState(false)
+ const [analysisContent, setAnalysisContent] = useState('')
+ const [renderTick, setRenderTick] = useState(0)
const aiMessagesRef = useRef(null)
const aiLoadedRef = useRef(false)
@@ -177,6 +252,21 @@ export default function Shell({ api }) {
aiMessagesRef.current?.scrollTo(0, aiMessagesRef.current.scrollHeight)
}, [aiMessages])
+ useEffect(() => {
+ const ms = aiLoading ? 1000 : 5000
+ const iv = setInterval(() => setRenderTick(t => t + 1), ms)
+ return () => clearInterval(iv)
+ }, [aiLoading])
+
+ useEffect(() => {
+ api.getShellAnalysis?.().then(d => {
+ if (d?.analysis) setAnalysisContent(d.analysis)
+ }).catch(() => {
+ const stored = localStorage.getItem('shell_analysis')
+ if (stored) setAnalysisContent(stored)
+ })
+ }, [])
+
useEffect(() => {
if (aiLoadedRef.current) return
aiLoadedRef.current = true
@@ -193,6 +283,11 @@ export default function Shell({ api }) {
})
}, [])
+ useEffect(() => {
+ const maxId = tabs.reduce((max, t) => Math.max(max, t.id), 0)
+ nextIdRef.current = maxId + 1
+ }, [])
+
useEffect(() => {
api.getTerminalSessions().then(d => {
setSshConnections(d.ssh || [])
@@ -213,12 +308,13 @@ export default function Shell({ api }) {
if (tabsRef.current[tabId]) return
const container = document.getElementById(`terminal-${tabId}`)
- if (!container) return
+ if (!container || container.offsetHeight === 0) return
+ const s = settingsRef.current
const { term, fitAddon } = createTerminal(container, {
- fontSize: terminalSettings.fontSize,
- fontFamily: terminalSettings.fontFamily,
- theme: terminalSettings.theme,
+ fontSize: s.fontSize,
+ fontFamily: s.fontFamily,
+ theme: s.theme,
})
let initPayload
@@ -271,26 +367,40 @@ export default function Shell({ api }) {
const tab = tabs.find(t => t.id === activeTab)
if (!tab) return
- const container = document.getElementById(`terminal-${tab.id}`)
- if (!container) return
-
- if (!tabsRef.current[tab.id]) {
- const timer = setTimeout(() => {
+ const tryInit = (attempt) => {
+ if (attempt > 10) return
+ const container = document.getElementById(`terminal-${tab.id}`)
+ if (!container || container.offsetHeight === 0) {
+ setTimeout(() => tryInit(attempt + 1), 100)
+ return
+ }
+ if (!tabsRef.current[tab.id]) {
initTerminal(tab.id, tab)
- requestAnimationFrame(() => {
- const entry = tabsRef.current[tab.id]
- if (entry) entry.fitAddon.fit()
- })
- }, 100)
- return () => clearTimeout(timer)
- } else {
+ }
requestAnimationFrame(() => {
const entry = tabsRef.current[tab.id]
if (entry) entry.fitAddon.fit()
})
}
+
+ tryInit(0)
}, [activeTab, tabs, initTerminal])
+ useEffect(() => {
+ const iv = setInterval(() => {
+ for (const tab of tabs) {
+ const entry = tabsRef.current[tab.id]
+ if (entry) {
+ const el = document.getElementById(`terminal-${tab.id}`)
+ if (el && el.offsetParent !== null) {
+ entry.fitAddon.fit()
+ }
+ }
+ }
+ }, 2000)
+ return () => clearInterval(iv)
+ }, [tabs])
+
useEffect(() => {
const onKey = (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return
@@ -309,8 +419,8 @@ export default function Shell({ api }) {
const addLocalTab = (shell, name) => {
if (tabs.length >= MAX_TABS) return
const id = nextIdRef.current++
- const newTab = { id, name: name || `${t('shell.localShell')} ${tabs.length + 1}`, type: 'local', shell: shell || '', connected: false }
- setTabs(prev => [...prev, newTab])
+ const newTab = { id, name: name || `${t('shell.localShell')} ${tabs.length}`, type: 'local', shell: shell || '', connected: false }
+ setTabs(prev => { const next = [...prev, newTab]; localStorage.setItem(TABS_STORAGE_KEY, JSON.stringify(next.map(t => ({ id: t.id, name: t.name, type: t.type, shell: t.shell, ai: t.ai || false, host: t.host, port: t.port, user: t.user, key_path: t.key_path })))); return next })
setActiveTab(id)
setShowMenu(false)
}
@@ -328,14 +438,15 @@ export default function Shell({ api }) {
key_path: conn.key_path || '',
connected: false,
}
- setTabs(prev => [...prev, newTab])
+ setTabs(prev => { const next = [...prev, newTab]; localStorage.setItem(TABS_STORAGE_KEY, JSON.stringify(next.map(t => ({ id: t.id, name: t.name, type: t.type, shell: t.shell, ai: t.ai || false, host: t.host, port: t.port, user: t.user, key_path: t.key_path })))); return next })
setActiveTab(id)
setShowMenu(false)
}
const closeTab = (tabId, e) => {
if (e) e.stopPropagation()
- if (tabs.length <= 1) return
+ const tab = tabs.find(t => t.id === tabId)
+ if (!tab || tab.ai || tabs.length <= 1) return
if (tabsRef.current[tabId]) {
const { ws, resizeObserver, onResize, term } = tabsRef.current[tabId]
@@ -392,17 +503,25 @@ export default function Shell({ api }) {
}
const sendToTerminal = useCallback((code) => {
- const tab = tabs.find(t => t.id === activeTab)
- if (!tab) return
- const entry = tabsRef.current[tab.id]
- if (!entry?.ws || entry.ws.readyState !== WebSocket.OPEN) return
- entry.ws.send(JSON.stringify({ type: 'input', data: code + '\r' }))
- }, [tabs, activeTab])
+ const aiEntry = tabsRef.current[AI_TAB_ID]
+ if (aiEntry?.ws && aiEntry.ws.readyState === WebSocket.OPEN) {
+ aiEntry.ws.send(JSON.stringify({ type: 'input', data: code + '\r' }))
+ }
+ }, [])
+
+ const focusAiTerminal = useCallback(() => {
+ setActiveTab(AI_TAB_ID)
+ setTimeout(() => {
+ const entry = tabsRef.current[AI_TAB_ID]
+ if (entry) entry.term.focus()
+ }, 150)
+ }, [])
const handleAiSend = async () => {
if (!aiInput.trim() || aiLoading || aiAtLimit) return
const text = aiInput.trim()
setAiInput('')
+ focusAiTerminal()
if (text === '/clear') {
try {
@@ -453,11 +572,73 @@ export default function Shell({ api }) {
setAiLoading(false)
}
+ useEffect(() => {
+ const handler = (e) => {
+ const msg = e.detail?.message
+ if (!msg) return
+ setAiInput(msg)
+ setActiveTab(AI_TAB_ID)
+ setTimeout(() => {
+ handleAiSendDirect(msg)
+ }, 100)
+ }
+ window.addEventListener('ask-ai-terminal', handler)
+ return () => window.removeEventListener('ask-ai-terminal', handler)
+ }, [])
+
+ const handleAiSendDirect = async (text) => {
+ if (!text || aiLoading || aiAtLimit) return
+ setAiInput('')
+
+ if (text === '/clear') {
+ try {
+ await api.clearShellChat()
+ setAiMessages([{ role: 'assistant', content: t('shell.aiWelcome') || 'Contexte effacé. Prêt.' }])
+ setAiTokens(0)
+ setAiAtLimit(false)
+ } catch {}
+ return
+ }
+
+ setAiMessages(prev => [...prev, { role: 'user', content: text }])
+ setAiLoading(true)
+
+ try {
+ let accumulated = ''
+ await api.sendShellChat(text, {}, true, (partial) => {
+ accumulated = partial
+ setAiMessages(prev => {
+ const filtered = prev.filter(m => !m._streaming)
+ return [...filtered, { role: 'assistant', content: partial, _streaming: true }]
+ })
+ })
+
+ setAiMessages(prev => {
+ const filtered = prev.filter(m => !m._streaming)
+ return [...filtered, { role: 'assistant', content: accumulated }]
+ })
+ api.getShellChatHistory().then(d => {
+ setAiTokens(d.tokens || 0)
+ setAiAtLimit(d.at_limit || false)
+ }).catch(() => {})
+ } catch (err) {
+ if (err.message.includes('context limit')) {
+ setAiAtLimit(true)
+ }
+ setAiMessages(prev => [...prev.filter(m => !m._streaming), { role: 'assistant', content: `Erreur: ${err.message}` }])
+ }
+ setAiLoading(false)
+ }
+
const handleAnalyze = async () => {
setAnalyzing(true)
setAiMessages(prev => [...prev, { role: 'system', content: 'Analyse du système en cours...' }])
try {
const d = await api.analyzeSystem()
+ if (d.analysis) {
+ setAnalysisContent(d.analysis)
+ localStorage.setItem('shell_analysis', d.analysis)
+ }
setAiMessages(prev => [...prev.filter(m => m.content !== 'Analyse du système en cours...'), {
role: 'system',
content: 'Analyse système terminée et sauvegardée. Le contexte système est maintenant disponible.'
@@ -476,13 +657,14 @@ export default function Shell({ api }) {
{tabs.map((tab, i) => (
setActiveTab(tab.id)}
- onDoubleClick={(e) => startRename(tab.id, e)}
+ onDoubleClick={(e) => !tab.ai && startRename(tab.id, e)}
>
- {tab.type === 'ssh' &&
}
- {tab.type === 'local' &&
}
+ {tab.ai &&
}
+ {!tab.ai && tab.type === 'ssh' &&
}
+ {!tab.ai && tab.type === 'local' &&
}
{editingTab === tab.id ? (
{tab.name}
)}
{i + 1}
- {tabs.length > 1 && (
+ {!tab.ai && tabs.length > 1 && (