fix(shell,config): terminal font size, AI tools, provider keys
All checks were successful
Beta Release / beta (push) Successful in 56s
All checks were successful
Beta Release / beta (push) Successful in 56s
- Fix terminal default fontSize from 6px to 14px across all references - Add terminal tool to shell AI via ChatEngine with tool_call streaming - Fix provider key detection (apiKey → api_key, baseURL → base_url) - Add mimo provider migration and validation endpoint - Bump version to 0.4.0 💘 Generated with Crush Assisted-by: GLM-5.1 via Crush <crush@charm.land>
This commit is contained in:
@@ -187,6 +187,8 @@ func (s *Server) handleValidateProvider(w http.ResponseWriter, r *http.Request)
|
||||
switch body.Name {
|
||||
case "minimax":
|
||||
baseURL = "https://api.minimax.io/v1"
|
||||
case "mimo":
|
||||
baseURL = "https://token-plan-ams.xiaomimimo.com/v1"
|
||||
case "openai":
|
||||
baseURL = "https://api.openai.com/v1"
|
||||
case "anthropic":
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -51,26 +52,31 @@ func (s *Server) handleShellChat(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
orb.SetSystemPrompt(s.buildShellSystemPromptV2(req))
|
||||
orb.SetSystemPrompt(s.buildShellSystemPrompt(req))
|
||||
orb.SetTools(s.shellAgentToolsJSON)
|
||||
|
||||
if req.Stream {
|
||||
s.handleShellChatStreamV2(w, orb)
|
||||
s.handleShellChatStream(w, orb)
|
||||
} else {
|
||||
s.handleShellChatNonStreamV2(w, orb)
|
||||
s.handleShellChatNonStream(w, orb)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) buildShellSystemPromptV2(_ ShellChatRequest) string {
|
||||
func (s *Server) buildShellSystemPrompt(_ ShellChatRequest) string {
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString(`Tu es l'Analyste Système de Muyue. Tu es un expert en administration système et développement.
|
||||
Tu aides l'utilisateur à comprendre son système, diagnostiquer des problèmes, et optimiser son environnement.
|
||||
|
||||
RÈGLES STRICTES:
|
||||
- Tu ne peux JAMAIS exécuter de commande ou de code
|
||||
- Tu ne peux que analyser, expliquer, et proposer des solutions
|
||||
- Quand tu proposes du code ou des commandes, mets-les dans des blocs de code markdown avec le langage spécifié
|
||||
- L'utilisateur pourra les copier ou les envoyer directement au terminal depuis les boutons
|
||||
OUTILS DISPONIBLES:
|
||||
- terminal: Exécute des commandes shell sur le système local et retourne le résultat
|
||||
|
||||
RÈGLES:
|
||||
- Utilise l'outil terminal pour exécuter des commandes quand c'est nécessaire
|
||||
- Analyse les résultats et explique-les clairement
|
||||
- Formate tes réponses en markdown avec des blocs de code quand approprié
|
||||
- Sois concis et technique
|
||||
- Quand tu proposes des commandes alternatives, utilise des blocs de code markdown
|
||||
|
||||
`)
|
||||
|
||||
@@ -89,43 +95,42 @@ RÈGLES STRICTES:
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func (s *Server) handleShellChatStreamV2(w http.ResponseWriter, orb *orchestrator.Orchestrator) {
|
||||
func (s *Server) handleShellChatStream(w http.ResponseWriter, orb *orchestrator.Orchestrator) {
|
||||
SetupSSEHeaders(w)
|
||||
flusher, canFlush := w.(http.Flusher)
|
||||
sseWriter := NewSSEWriter(w)
|
||||
|
||||
// Rebuild history into orchestrator
|
||||
history := s.shellConvStore.Get()
|
||||
for _, m := range history[:len(history)-1] { // all except last user msg
|
||||
if m.Role == "system" {
|
||||
continue
|
||||
ctx := context.Background()
|
||||
messages := s.buildShellContextMessages()
|
||||
|
||||
engine := NewChatEngine(orb, s.shellAgentRegistry, s.shellAgentToolsJSON)
|
||||
engine.OnChunk(func(data map[string]interface{}) {
|
||||
if data == nil {
|
||||
return
|
||||
}
|
||||
// Pre-load orchestrator history
|
||||
orb.AppendHistory(orchestrator.Message{Role: m.Role, Content: m.Content})
|
||||
}
|
||||
|
||||
lastUserMsg := history[len(history)-1].Content
|
||||
|
||||
var finalContent string
|
||||
result, err := orb.SendStream(lastUserMsg, func(chunk string) {
|
||||
finalContent = chunk
|
||||
sseWriter.Write(map[string]interface{}{"content": chunk})
|
||||
sseWriter.Write(data)
|
||||
if canFlush {
|
||||
flusher.Flush()
|
||||
}
|
||||
})
|
||||
|
||||
finalContent, allToolCalls, allToolResults, err := engine.RunWithTools(ctx, messages)
|
||||
if err != nil {
|
||||
sseWriter.Write(map[string]interface{}{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
content := result
|
||||
if content == "" {
|
||||
content = finalContent
|
||||
storeContent := finalContent
|
||||
if len(allToolCalls) > 0 {
|
||||
storeObj := map[string]interface{}{
|
||||
"content": storeContent,
|
||||
"tool_calls": allToolCalls,
|
||||
"tool_results": allToolResults,
|
||||
}
|
||||
storeJSON, _ := json.Marshal(storeObj)
|
||||
storeContent = string(storeJSON)
|
||||
}
|
||||
|
||||
s.shellConvStore.Add("assistant", cleanThinkingTags(content))
|
||||
s.shellConvStore.Add("assistant", storeContent)
|
||||
|
||||
sseWriter.Write(map[string]interface{}{
|
||||
"done": "true",
|
||||
@@ -133,30 +138,62 @@ func (s *Server) handleShellChatStreamV2(w http.ResponseWriter, orb *orchestrato
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleShellChatNonStreamV2(w http.ResponseWriter, orb *orchestrator.Orchestrator) {
|
||||
history := s.shellConvStore.Get()
|
||||
for _, m := range history[:len(history)-1] {
|
||||
if m.Role == "system" {
|
||||
continue
|
||||
}
|
||||
orb.AppendHistory(orchestrator.Message{Role: m.Role, Content: m.Content})
|
||||
}
|
||||
func (s *Server) handleShellChatNonStream(w http.ResponseWriter, orb *orchestrator.Orchestrator) {
|
||||
ctx := context.Background()
|
||||
messages := s.buildShellContextMessages()
|
||||
|
||||
lastUserMsg := history[len(history)-1].Content
|
||||
|
||||
result, err := orb.Send(lastUserMsg)
|
||||
engine := NewChatEngine(orb, s.shellAgentRegistry, s.shellAgentToolsJSON)
|
||||
finalContent, err := engine.RunNonStream(ctx, messages)
|
||||
if err != nil {
|
||||
writeError(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
s.shellConvStore.Add("assistant", cleanThinkingTags(result))
|
||||
s.shellConvStore.Add("assistant", finalContent)
|
||||
writeJSON(w, map[string]interface{}{
|
||||
"content": result,
|
||||
"content": finalContent,
|
||||
"tokens": s.shellConvStore.ApproxTokens(),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) buildShellContextMessages() []orchestrator.Message {
|
||||
history := s.shellConvStore.Get()
|
||||
start := 0
|
||||
const shellContextWindow = 20
|
||||
if len(history) > shellContextWindow {
|
||||
start = len(history) - shellContextWindow
|
||||
}
|
||||
|
||||
messages := make([]orchestrator.Message, 0, len(history[start:]))
|
||||
|
||||
for _, m := range history[start:] {
|
||||
content := m.Content
|
||||
if m.Role == "assistant" {
|
||||
var parsed struct {
|
||||
Content string `json:"content"`
|
||||
ToolCalls []struct {
|
||||
ToolCallID string `json:"tool_call_id"`
|
||||
Name string `json:"name"`
|
||||
Args string `json:"args"`
|
||||
} `json:"tool_calls"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(content), &parsed); err == nil && parsed.Content != "" {
|
||||
content = parsed.Content
|
||||
}
|
||||
}
|
||||
role := m.Role
|
||||
if role == "system" {
|
||||
continue
|
||||
}
|
||||
messages = append(messages, orchestrator.Message{
|
||||
Role: role,
|
||||
Content: content,
|
||||
})
|
||||
}
|
||||
|
||||
return messages
|
||||
}
|
||||
|
||||
func (s *Server) handleShellChatHistory(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" {
|
||||
writeError(w, "GET only", http.StatusMethodNotAllowed)
|
||||
|
||||
@@ -13,14 +13,16 @@ import (
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
config *config.MuyueConfig
|
||||
scanResult *scanner.ScanResult
|
||||
mux *http.ServeMux
|
||||
convStore *ConversationStore
|
||||
shellConvStore *ShellConvStore
|
||||
agentRegistry *agent.Registry
|
||||
agentToolsJSON json.RawMessage
|
||||
workflowEngine *workflow.Engine
|
||||
config *config.MuyueConfig
|
||||
scanResult *scanner.ScanResult
|
||||
mux *http.ServeMux
|
||||
convStore *ConversationStore
|
||||
shellConvStore *ShellConvStore
|
||||
agentRegistry *agent.Registry
|
||||
agentToolsJSON json.RawMessage
|
||||
shellAgentRegistry *agent.Registry
|
||||
shellAgentToolsJSON json.RawMessage
|
||||
workflowEngine *workflow.Engine
|
||||
}
|
||||
|
||||
func NewServer(cfg *config.MuyueConfig) *Server {
|
||||
@@ -52,6 +54,14 @@ func NewServer(cfg *config.MuyueConfig) *Server {
|
||||
tools := s.agentRegistry.OpenAITools()
|
||||
toolsJSON, _ := json.Marshal(tools)
|
||||
s.agentToolsJSON = json.RawMessage(toolsJSON)
|
||||
|
||||
s.shellAgentRegistry = agent.NewRegistry()
|
||||
terminalTool, _ := agent.NewTerminalTool()
|
||||
s.shellAgentRegistry.Register(terminalTool)
|
||||
shellTools := s.shellAgentRegistry.OpenAITools()
|
||||
shellToolsJSON, _ := json.Marshal(shellTools)
|
||||
s.shellAgentToolsJSON = json.RawMessage(shellToolsJSON)
|
||||
|
||||
s.workflowEngine, _ = workflow.NewEngine(s.agentRegistry)
|
||||
s.routes()
|
||||
return s
|
||||
|
||||
@@ -128,6 +128,22 @@ var DEFAULT_TERMINAL_THEMES = map[string]TerminalTheme{
|
||||
},
|
||||
}
|
||||
|
||||
func migrateProviders(cfg *MuyueConfig) {
|
||||
defaults := Default().AI.Providers
|
||||
for _, dp := range defaults {
|
||||
found := false
|
||||
for _, p := range cfg.AI.Providers {
|
||||
if p.Name == dp.Name {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
cfg.AI.Providers = append(cfg.AI.Providers, dp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func GetTerminalTheme(name string) TerminalTheme {
|
||||
if theme, ok := DEFAULT_TERMINAL_THEMES[name]; ok {
|
||||
return theme
|
||||
@@ -206,6 +222,8 @@ func Load() (*MuyueConfig, error) {
|
||||
}
|
||||
}
|
||||
|
||||
migrateProviders(&cfg)
|
||||
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
@@ -303,6 +321,7 @@ func Default() *MuyueConfig {
|
||||
|
||||
cfg.Terminal.CustomPrompt = true
|
||||
cfg.Terminal.PromptTheme = "zerotwo"
|
||||
cfg.Terminal.FontSize = 14
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
const (
|
||||
Name = "muyue"
|
||||
Version = "0.3.5"
|
||||
Version = "0.4.0"
|
||||
Author = "La Légion de Muyue"
|
||||
)
|
||||
|
||||
|
||||
@@ -141,7 +141,11 @@ const api = {
|
||||
if (data.error) { reject(new Error(data.error)); return }
|
||||
if (data.done) { resolve({ content: full, tokens: data.tokens }); return }
|
||||
if (data.content) {
|
||||
full = data.content
|
||||
full += data.content
|
||||
if (onChunk) onChunk(full, data)
|
||||
} else if (data.tool_call || data.tool_result) {
|
||||
if (onChunk) onChunk(full, data)
|
||||
} else if (data.thinking !== undefined || data.thinking_end) {
|
||||
if (onChunk) onChunk(full, data)
|
||||
}
|
||||
} catch {}
|
||||
|
||||
@@ -101,9 +101,9 @@ export default function Config({ api }) {
|
||||
...prev,
|
||||
[p.name]: {
|
||||
name: p.name,
|
||||
api_key: p.apiKey || '',
|
||||
api_key: p.api_key || '',
|
||||
model: p.model || '',
|
||||
base_url: p.baseURL || '',
|
||||
base_url: p.base_url || '',
|
||||
},
|
||||
}))
|
||||
setEditProvider(p.name)
|
||||
@@ -314,7 +314,7 @@ function PanelProviders({ providers, editProvider, providerForm, setProviderForm
|
||||
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 || '' })
|
||||
await api.validateProvider({ name: p.name, api_key: p.api_key, model: p.model, base_url: p.base_url || '' })
|
||||
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' } }))
|
||||
@@ -324,9 +324,9 @@ function PanelProviders({ providers, editProvider, providerForm, setProviderForm
|
||||
|
||||
useEffect(() => {
|
||||
providers.forEach(p => {
|
||||
if (p.apiKey && !keyStatus[p.name]) {
|
||||
if (p.api_key && !keyStatus[p.name]) {
|
||||
validateKey(p)
|
||||
} else if (!p.apiKey) {
|
||||
} else if (!p.api_key) {
|
||||
setKeyStatus(prev => ({ ...prev, [p.name]: { valid: false, checked: true, error: 'Aucune clé' } }))
|
||||
}
|
||||
})
|
||||
@@ -370,7 +370,7 @@ function PanelProviders({ providers, editProvider, providerForm, setProviderForm
|
||||
<input
|
||||
className="config-form-input"
|
||||
type="password"
|
||||
placeholder={p.apiKey ? '••••••••' : t('config.tokenPlaceholder')}
|
||||
placeholder={p.api_key ? '••••••••' : t('config.tokenPlaceholder')}
|
||||
value={isEditing ? (providerForm[p.name]?.api_key || '') : ''}
|
||||
onChange={e => {
|
||||
if (!isEditing) openProviderEdit(p)
|
||||
|
||||
@@ -204,7 +204,7 @@ function createTerminal(container, settings = {}) {
|
||||
const term = new XTerm({
|
||||
cursorBlink: true,
|
||||
allowProposedApi: true,
|
||||
fontSize: settings.fontSize || 6,
|
||||
fontSize: settings.fontSize || 14,
|
||||
fontFamily: settings.fontFamily || "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', 'Menlo', monospace",
|
||||
theme,
|
||||
allowTransparency: false,
|
||||
@@ -350,7 +350,7 @@ export default function Shell({ api }) {
|
||||
const { t } = useI18n()
|
||||
const tabsRef = useRef({})
|
||||
const nextIdRef = useRef(1)
|
||||
const settingsRef = useRef({ fontSize: 6, fontFamily: "'JetBrains Mono', 'Fira Code', monospace", theme: 'system' })
|
||||
const settingsRef = useRef({ fontSize: 14, fontFamily: "'JetBrains Mono', 'Fira Code', monospace", theme: 'system' })
|
||||
const pendingCommandsRef = useRef({})
|
||||
|
||||
const [tabs, setTabs] = useState(() => {
|
||||
@@ -399,7 +399,7 @@ export default function Shell({ api }) {
|
||||
const [editingTab, setEditingTab] = useState(null)
|
||||
const [editName, setEditName] = useState('')
|
||||
const [terminalSettings, setTerminalSettings] = useState({
|
||||
fontSize: 6,
|
||||
fontSize: 14,
|
||||
fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', 'Menlo', monospace",
|
||||
theme: 'system',
|
||||
})
|
||||
@@ -414,7 +414,7 @@ export default function Shell({ api }) {
|
||||
useEffect(() => { settingsRef.current = terminalSettings }, [terminalSettings])
|
||||
|
||||
useEffect(() => {
|
||||
baseFontSizeRef.current = terminalSettings.fontSize || 6
|
||||
baseFontSizeRef.current = terminalSettings.fontSize || 14
|
||||
}, [terminalSettings.fontSize])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -496,7 +496,7 @@ export default function Shell({ api }) {
|
||||
api.getConfig().then(d => {
|
||||
if (d.terminal) {
|
||||
setTerminalSettings({
|
||||
fontSize: d.terminal.font_size || 6,
|
||||
fontSize: d.terminal.font_size || 14,
|
||||
fontFamily: d.terminal.font_family || "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', 'Menlo', monospace",
|
||||
theme: d.terminal.theme || 'system',
|
||||
})
|
||||
@@ -1000,7 +1000,7 @@ export default function Shell({ api }) {
|
||||
if (trimmed === '/help') {
|
||||
setAiMessages(prev => [...prev,
|
||||
{ role: 'user', content: trimmed },
|
||||
{ role: 'assistant', content: 'Commandes disponibles:\n• /clear — Effacer la conversation\n• /help — Afficher l\'aide\n\nJe ne peux pas exécuter de code. Les blocs de code proposés peuvent être copiés ou envoyés directement au terminal actif.' }
|
||||
{ role: 'assistant', content: 'Commandes disponibles:\n• /clear — Effacer la conversation\n• /help — Afficher l\'aide\n\nJe peux exécuter des commandes via l\'outil terminal. Les blocs de code proposés peuvent aussi être copiés ou envoyés directement au terminal actif.' }
|
||||
])
|
||||
aiLoadingRef.current = false
|
||||
return
|
||||
@@ -1013,17 +1013,56 @@ export default function Shell({ api }) {
|
||||
|
||||
try {
|
||||
let accumulated = ''
|
||||
await api.sendShellChat(trimmed, {}, true, (partial) => {
|
||||
let toolCalls = []
|
||||
const controller = new AbortController()
|
||||
|
||||
await api.sendShellChat(trimmed, {}, true, (partial, event) => {
|
||||
if (event && event.tool_call) {
|
||||
toolCalls = [...toolCalls, { call: event.tool_call, result: null }]
|
||||
setAiMessages(prev => {
|
||||
const filtered = prev.filter(m => !m._streaming)
|
||||
return [...filtered, { role: 'assistant', content: accumulated, _streaming: true, _tabId: currentTab, _toolCalls: [...toolCalls] }]
|
||||
})
|
||||
return
|
||||
}
|
||||
if (event && event.tool_result) {
|
||||
const idx = toolCalls.findIndex(tc => tc.call && tc.call.tool_call_id === event.tool_result.tool_call_id)
|
||||
if (idx >= 0) {
|
||||
toolCalls[idx] = { ...toolCalls[idx], result: event.tool_result }
|
||||
setAiMessages(prev => {
|
||||
const filtered = prev.filter(m => !m._streaming)
|
||||
return [...filtered, { role: 'assistant', content: accumulated, _streaming: true, _tabId: currentTab, _toolCalls: [...toolCalls] }]
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
if (event && (event.thinking !== undefined || event.thinking_end)) {
|
||||
return
|
||||
}
|
||||
accumulated = partial
|
||||
setAiMessages(prev => {
|
||||
const filtered = prev.filter(m => !m._streaming)
|
||||
return [...filtered, { role: 'assistant', content: partial, _streaming: true, _tabId: currentTab }]
|
||||
return [...filtered, { role: 'assistant', content: partial, _streaming: true, _tabId: currentTab, _toolCalls: toolCalls.length > 0 ? [...toolCalls] : undefined }]
|
||||
})
|
||||
})
|
||||
}, controller.signal)
|
||||
|
||||
const finalMsg = { role: 'assistant', content: accumulated, _tabId: currentTab }
|
||||
if (toolCalls.length > 0) {
|
||||
finalMsg._toolCalls = toolCalls
|
||||
finalMsg.content = JSON.stringify({
|
||||
content: accumulated,
|
||||
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,
|
||||
})),
|
||||
})
|
||||
}
|
||||
|
||||
setAiMessages(prev => {
|
||||
const filtered = prev.filter(m => !m._streaming)
|
||||
return [...filtered, { role: 'assistant', content: accumulated, _tabId: currentTab }]
|
||||
return [...filtered, finalMsg]
|
||||
})
|
||||
api.getShellChatHistory().then(d => {
|
||||
setAiTokens(d.tokens || 0)
|
||||
@@ -1336,6 +1375,40 @@ export default function Shell({ api }) {
|
||||
)
|
||||
}
|
||||
|
||||
function ShellToolBlock({ call, result }) {
|
||||
const icon = '⌨'
|
||||
const label = call.name === 'terminal' ? 'Terminal' : call.name
|
||||
const isErr = result && result.is_error
|
||||
|
||||
let argsPreview = ''
|
||||
try {
|
||||
const args = typeof call.args === 'string' ? JSON.parse(call.args) : call.args
|
||||
if (args.command) argsPreview = args.command
|
||||
else argsPreview = JSON.stringify(args).slice(0, 80)
|
||||
} catch {
|
||||
argsPreview = String(call.args).slice(0, 80)
|
||||
}
|
||||
|
||||
const truncatedResult = result ? (result.content || '').slice(0, 1500) : null
|
||||
|
||||
return (
|
||||
<div className={`studio-tool-block ${isErr ? 'error' : ''} ${result ? 'done' : 'running'}`}>
|
||||
<div className="studio-tool-header">
|
||||
<span className="studio-tool-icon">{icon}</span>
|
||||
<span className="studio-tool-name">{label}</span>
|
||||
{!result && <span className="studio-tool-spinner"><span/><span/><span/></span>}
|
||||
{result && <span className={`studio-tool-status ${isErr ? 'error' : 'ok'}`}>{isErr ? '✗' : '✓'}</span>}
|
||||
</div>
|
||||
<div className="studio-tool-args" title={argsPreview}>{argsPreview}</div>
|
||||
{truncatedResult && (
|
||||
<div className="studio-tool-result">
|
||||
<pre>{truncatedResult}</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ShellAIMessage({ msg, sendToTerminal, terminalTabId }) {
|
||||
const role = msg.role === 'user' ? 'user' : msg.role === 'system' ? 'system' : 'assistant'
|
||||
const content = msg.content || ''
|
||||
@@ -1348,10 +1421,38 @@ function ShellAIMessage({ msg, sendToTerminal, terminalTabId }) {
|
||||
return <div className={`ai-message system`}>{content}</div>
|
||||
}
|
||||
|
||||
const parts = renderContent(content)
|
||||
let parsedToolCalls = null
|
||||
let parsedToolResults = null
|
||||
let displayContent = content
|
||||
let streamingToolCalls = msg._toolCalls || null
|
||||
|
||||
if (!streamingToolCalls) {
|
||||
try {
|
||||
const parsed = JSON.parse(content)
|
||||
if (parsed && Array.isArray(parsed.tool_calls)) {
|
||||
parsedToolCalls = parsed.tool_calls
|
||||
parsedToolResults = parsed.tool_results || null
|
||||
displayContent = parsed.content || ''
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const parts = renderContent(displayContent)
|
||||
|
||||
return (
|
||||
<div className={`ai-message assistant`}>
|
||||
{streamingToolCalls && streamingToolCalls.map((tc, i) => (
|
||||
<ShellToolBlock key={tc.call?.tool_call_id || i} call={tc.call} result={tc.result} />
|
||||
))}
|
||||
{parsedToolCalls && parsedToolCalls.map((tc, i) => {
|
||||
const resultData = parsedToolResults
|
||||
? parsedToolResults.find(r => r.tool_call_id === tc.tool_call_id)
|
||||
: null
|
||||
const result = resultData
|
||||
? { content: resultData.result, is_error: resultData.is_error }
|
||||
: null
|
||||
return <ShellToolBlock key={tc.tool_call_id || i} call={tc} result={result} />
|
||||
})}
|
||||
{parts.map((part, i) => {
|
||||
if (part.type === 'code') {
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user