Compare commits
5 Commits
v0.3.5-bet
...
v0.3.5-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cbbb224725 | ||
|
|
8d10d2182e | ||
|
|
e9696ef82b | ||
|
|
1edd4f053a | ||
|
|
92f943c3e6 |
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react'
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
import { User, Brain, RefreshCw, Wrench, Monitor, AlertTriangle, X } from 'lucide-react'
|
import { User, Brain, RefreshCw, Wrench, Monitor, AlertTriangle } from 'lucide-react'
|
||||||
import { useI18n } from '../i18n'
|
import { useI18n } from '../i18n'
|
||||||
|
|
||||||
const PANELS = [
|
const PANELS = [
|
||||||
@@ -311,16 +311,6 @@ function PanelProviders({ providers, editProvider, providerForm, setProviderForm
|
|||||||
const [validating, setValidating] = useState(null)
|
const [validating, setValidating] = useState(null)
|
||||||
const [keyStatus, setKeyStatus] = useState({})
|
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) => {
|
const validateKey = async (p) => {
|
||||||
setValidating(p.name)
|
setValidating(p.name)
|
||||||
try {
|
try {
|
||||||
@@ -332,6 +322,16 @@ function PanelProviders({ providers, editProvider, providerForm, setProviderForm
|
|||||||
setValidating(null)
|
setValidating(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 handleValidate = async (name, apiKey, model, baseUrl) => {
|
const handleValidate = async (name, apiKey, model, baseUrl) => {
|
||||||
setValidating(name)
|
setValidating(name)
|
||||||
try {
|
try {
|
||||||
@@ -412,7 +412,7 @@ function PanelUpdates({ updates, tools, checking, updating, needsUpdateCount, in
|
|||||||
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.` } }))
|
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)
|
const missingTools = tools.filter(tool => !tool.installed)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -204,6 +204,7 @@ export default function Shell({ api }) {
|
|||||||
const tabsRef = useRef({})
|
const tabsRef = useRef({})
|
||||||
const nextIdRef = useRef(1)
|
const nextIdRef = useRef(1)
|
||||||
const settingsRef = useRef({ fontSize: 12, fontFamily: "'JetBrains Mono', 'Fira Code', monospace", theme: 'default' })
|
const settingsRef = useRef({ fontSize: 12, fontFamily: "'JetBrains Mono', 'Fira Code', monospace", theme: 'default' })
|
||||||
|
const pendingCommandsRef = useRef({})
|
||||||
|
|
||||||
const savedTabs = (() => {
|
const savedTabs = (() => {
|
||||||
try {
|
try {
|
||||||
@@ -227,6 +228,8 @@ export default function Shell({ api }) {
|
|||||||
}
|
}
|
||||||
return 1
|
return 1
|
||||||
})
|
})
|
||||||
|
const activeTabRef = useRef(activeTab)
|
||||||
|
useEffect(() => { activeTabRef.current = activeTab }, [activeTab])
|
||||||
const [sshConnections, setSshConnections] = useState([])
|
const [sshConnections, setSshConnections] = useState([])
|
||||||
const [systemTerminals, setSystemTerminals] = useState([])
|
const [systemTerminals, setSystemTerminals] = useState([])
|
||||||
const [showMenu, setShowMenu] = useState(false)
|
const [showMenu, setShowMenu] = useState(false)
|
||||||
@@ -408,9 +411,21 @@ export default function Shell({ api }) {
|
|||||||
|
|
||||||
const bufferSaveInterval = setInterval(() => { if (!disposed) saveBuffer() }, 5000)
|
const bufferSaveInterval = setInterval(() => { if (!disposed) saveBuffer() }, 5000)
|
||||||
|
|
||||||
|
console.log(`[Shell] initTerminal tab=${tabId} type=${tab.type} name="${tab.name}" shell="${tab.shell || '(default)'}"`)
|
||||||
tabsRef.current[tabId] = { term, fitAddon, ws, resizeObserver, onResize, bufferSaveInterval, saveBuffer, disposed: () => disposed }
|
tabsRef.current[tabId] = { term, fitAddon, ws, resizeObserver, onResize, bufferSaveInterval, saveBuffer, disposed: () => disposed }
|
||||||
const origDispose = () => { disposed = true }
|
tabsRef.current[tabId]._markDisposed = () => { disposed = true }
|
||||||
tabsRef.current[tabId]._markDisposed = origDispose
|
console.log(`[Shell] initTerminal tab=${tabId} done, tabsRef keys:`, Object.keys(tabsRef.current))
|
||||||
|
|
||||||
|
const pending = pendingCommandsRef.current[tabId]
|
||||||
|
if (pending && pending.length > 0) {
|
||||||
|
console.log(`[Shell] Flushing ${pending.length} pending commands for tab ${tabId}`)
|
||||||
|
for (const cmd of pending) {
|
||||||
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify({ type: 'input', data: cmd + '\r' }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete pendingCommandsRef.current[tabId]
|
||||||
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -598,23 +613,28 @@ export default function Shell({ api }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sendToTerminal = useCallback((code, tabId) => {
|
const sendToTerminal = useCallback((code, tabId) => {
|
||||||
const targetId = tabId || activeTab
|
const targetId = tabId || activeTabRef.current
|
||||||
const entry = tabsRef.current[targetId]
|
const entry = tabsRef.current[targetId]
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
console.warn('sendToTerminal: no terminal initialized for tab', targetId)
|
console.warn(`[Shell] sendToTerminal: tab ${targetId} not ready. Queueing. tabsRef:`, Object.keys(tabsRef.current), 'activeTab:', activeTabRef.current, 'requested:', tabId)
|
||||||
|
if (!pendingCommandsRef.current[targetId]) pendingCommandsRef.current[targetId] = []
|
||||||
|
pendingCommandsRef.current[targetId].push(code)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!entry.ws || entry.ws.readyState !== WebSocket.OPEN) {
|
if (!entry.ws || entry.ws.readyState !== WebSocket.OPEN) {
|
||||||
console.warn('sendToTerminal: WebSocket not ready for tab', targetId)
|
console.warn(`[Shell] sendToTerminal: WS not open for tab ${targetId} (state=${entry.ws?.readyState}). Queueing.`)
|
||||||
|
if (!pendingCommandsRef.current[targetId]) pendingCommandsRef.current[targetId] = []
|
||||||
|
pendingCommandsRef.current[targetId].push(code)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
console.log(`[Shell] sendToTerminal: tab ${targetId} ← ${code.length} chars`)
|
||||||
entry.ws.send(JSON.stringify({ type: 'input', data: code + '\r' }))
|
entry.ws.send(JSON.stringify({ type: 'input', data: code + '\r' }))
|
||||||
}, [activeTab])
|
}, [])
|
||||||
|
|
||||||
const focusAiTerminal = useCallback(() => {
|
const focusAiTerminal = useCallback(() => {
|
||||||
const entry = tabsRef.current[activeTab]
|
const entry = tabsRef.current[activeTabRef.current]
|
||||||
if (entry) entry.term.focus()
|
if (entry) entry.term.focus()
|
||||||
}, [activeTab])
|
}, [])
|
||||||
|
|
||||||
const _sendAiMessage = useCallback(async (text, fromEvent = false) => {
|
const _sendAiMessage = useCallback(async (text, fromEvent = false) => {
|
||||||
if (!text || !text.trim() || aiLoadingRef.current || aiAtLimit) return
|
if (!text || !text.trim() || aiLoadingRef.current || aiAtLimit) return
|
||||||
@@ -646,7 +666,9 @@ export default function Shell({ api }) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setAiMessages(prev => [...prev, { role: 'user', content: trimmed }])
|
const currentTab = activeTabRef.current
|
||||||
|
console.log(`[Shell] _sendAiMessage: activeTab=${currentTab}, fromEvent=${fromEvent}, text="${trimmed.slice(0, 50)}"`)
|
||||||
|
setAiMessages(prev => [...prev, { role: 'user', content: trimmed, _tabId: currentTab }])
|
||||||
setAiLoading(true)
|
setAiLoading(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -655,13 +677,13 @@ export default function Shell({ api }) {
|
|||||||
accumulated = partial
|
accumulated = partial
|
||||||
setAiMessages(prev => {
|
setAiMessages(prev => {
|
||||||
const filtered = prev.filter(m => !m._streaming)
|
const filtered = prev.filter(m => !m._streaming)
|
||||||
return [...filtered, { role: 'assistant', content: partial, _streaming: true }]
|
return [...filtered, { role: 'assistant', content: partial, _streaming: true, _tabId: currentTab }]
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
setAiMessages(prev => {
|
setAiMessages(prev => {
|
||||||
const filtered = prev.filter(m => !m._streaming)
|
const filtered = prev.filter(m => !m._streaming)
|
||||||
return [...filtered, { role: 'assistant', content: accumulated }]
|
return [...filtered, { role: 'assistant', content: accumulated, _tabId: currentTab }]
|
||||||
})
|
})
|
||||||
api.getShellChatHistory().then(d => {
|
api.getShellChatHistory().then(d => {
|
||||||
setAiTokens(d.tokens || 0)
|
setAiTokens(d.tokens || 0)
|
||||||
@@ -858,7 +880,7 @@ export default function Shell({ api }) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="ai-panel-messages" ref={aiMessagesRef}>
|
<div className="ai-panel-messages" ref={aiMessagesRef}>
|
||||||
{aiMessages.map((msg, i) => (
|
{aiMessages.map((msg, i) => (
|
||||||
<ShellAIMessage key={i} msg={msg} sendToTerminal={sendToTerminal} terminalTabId={activeTab} />
|
<ShellAIMessage key={i} msg={msg} sendToTerminal={sendToTerminal} terminalTabId={msg._tabId || activeTab} />
|
||||||
))}
|
))}
|
||||||
{aiLoading && <div style={{ textAlign: 'center', padding: 8 }}><span className="spinner" /></div>}
|
{aiLoading && <div style={{ textAlign: 'center', padding: 8 }}><span className="spinner" /></div>}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ function FeedItem({ msg }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const cleanContent = displayContent.replace(/<think[^>]*>[\s\S]*?<\/think>/gi, '')
|
let cleanContent = displayContent.replace(/<think[^>]*>[\s\S]*?<\/think>/gi, '')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`feed-item ${msg.role}`}>
|
<div className={`feed-item ${msg.role}`}>
|
||||||
@@ -532,6 +532,8 @@ export default function Studio({ api }) {
|
|||||||
if (event && event.tool_call) {
|
if (event && event.tool_call) {
|
||||||
toolCalls = [...toolCalls, { call: event.tool_call, result: null }]
|
toolCalls = [...toolCalls, { call: event.tool_call, result: null }]
|
||||||
setStreamToolCalls([...toolCalls])
|
setStreamToolCalls([...toolCalls])
|
||||||
|
accumulated = ''
|
||||||
|
setStreaming('')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (event && event.tool_result) {
|
if (event && event.tool_result) {
|
||||||
@@ -558,6 +560,11 @@ export default function Studio({ api }) {
|
|||||||
aiMsg.content = JSON.stringify({
|
aiMsg.content = JSON.stringify({
|
||||||
content: finalContent,
|
content: finalContent,
|
||||||
tool_calls: toolCalls.map(tc => tc.call),
|
tool_calls: toolCalls.map(tc => tc.call),
|
||||||
|
tool_results: toolCalls.map(tc => ({
|
||||||
|
tool_call_id: tc.call?.tool_call_id,
|
||||||
|
result: tc.result?.content || '',
|
||||||
|
is_error: tc.result?.is_error || false,
|
||||||
|
})),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
setMessages(prev => [...prev, aiMsg])
|
setMessages(prev => [...prev, aiMsg])
|
||||||
|
|||||||
Reference in New Issue
Block a user