feat(studio): parse AI thinking and tool launch messages in terminal panel

- Add message type detection: thinking (Reflexion/Thought/>), tool (TOOL_CALL),
  and normal AI responses
- Style thinking messages with italic blue, tool messages with yellow border
- Add toolLaunched i18n key for both fr and en locales

💘 Generated with Crush

Assisted-by: MiniMax-M2.7 via Crush <crush@charm.land>
This commit is contained in:
Augustin
2026-04-22 19:41:42 +02:00
parent 5ec373cd6a
commit 8fb93fa47e
4 changed files with 56 additions and 1 deletions

View File

@@ -380,13 +380,61 @@ export default function Shell({ api }) {
setAiLoading(true) setAiLoading(true)
try { try {
const res = await api.runCommand(`echo "AI: ${text}"`, '') const res = await api.runCommand(`echo "AI: ${text}"`, '')
setAiMessages(prev => [...prev, { role: 'ai', content: res.output || t('shell.noResponse') }]) const output = res.output || t('shell.noResponse')
parseAndAddAiMessages(output)
} catch (err) { } catch (err) {
setAiMessages(prev => [...prev, { role: 'ai', content: `${t('shell.error')}: ${err.message}` }]) setAiMessages(prev => [...prev, { role: 'ai', content: `${t('shell.error')}: ${err.message}` }])
} }
setAiLoading(false) setAiLoading(false)
} }
const parseAndAddAiMessages = (text) => {
const lines = text.split('\n')
let buffer = ''
let inBlock = false
const flushBuffer = () => {
if (buffer.trim()) {
setAiMessages(prev => [...prev, { role: 'ai', content: buffer.trim() }])
}
buffer = ''
}
for (const line of lines) {
const toolMatch = line.match(/^\[TOOL_CALL:\{.*\}\]$/)
if (toolMatch) {
flushBuffer()
try {
const toolData = JSON.parse(toolMatch[0].slice(10, -1))
setAiMessages(prev => [...prev, {
role: 'tool',
content: `${t('shell.toolLaunched')}: ${toolData.tool || 'tool'}`,
args: toolData.task || toolData.args || '',
}])
} catch {
setAiMessages(prev => [...prev, { role: 'tool', content: line, args: '' }])
}
} else if (line.match(/^(Reflexion|Thought|thinking):/i) || line.startsWith('>')) {
if (buffer.trim() && !inBlock) {
flushBuffer()
}
inBlock = true
const cleaned = line.replace(/^(Reflexion|Thought|thinking):\s*/i, '').replace(/^>\s*/, '')
if (buffer) buffer += ' '
buffer += cleaned
} else {
if (inBlock && buffer.trim()) {
setAiMessages(prev => [...prev, { role: 'thinking', content: buffer.trim() }])
buffer = ''
}
inBlock = false
if (buffer) buffer += '\n'
buffer += line
}
}
flushBuffer()
}
return ( return (
<div className="shell-layout"> <div className="shell-layout">
<div className="shell-terminal-col"> <div className="shell-terminal-col">
@@ -507,6 +555,7 @@ export default function Shell({ api }) {
{aiMessages.map((msg, i) => ( {aiMessages.map((msg, i) => (
<div key={i} className={`ai-message ${msg.role}`}> <div key={i} className={`ai-message ${msg.role}`}>
{msg.content} {msg.content}
{msg.args && <div className="tool-args">{msg.args}</div>}
</div> </div>
))} ))}
{aiLoading && <div style={{ textAlign: 'center', padding: 8 }}><span className="spinner" /></div>} {aiLoading && <div style={{ textAlign: 'center', padding: 8 }}><span className="spinner" /></div>}

View File

@@ -110,6 +110,7 @@ const en = {
aiAssistant: 'AI Assistant', aiAssistant: 'AI Assistant',
aiWelcome: 'Hello! I can help you with terminal commands. Ask me anything!', aiWelcome: 'Hello! I can help you with terminal commands. Ask me anything!',
askAi: 'Ask AI assistant...', askAi: 'Ask AI assistant...',
toolLaunched: 'Tool launched',
}, },
config: { config: {

View File

@@ -110,6 +110,7 @@ const fr = {
aiAssistant: 'Assistant IA', aiAssistant: 'Assistant IA',
aiWelcome: 'Bonjour ! Je peux vous aider avec les commandes du terminal. Demandez-moi n\'importe quoi !', aiWelcome: 'Bonjour ! Je peux vous aider avec les commandes du terminal. Demandez-moi n\'importe quoi !',
askAi: 'Interroger l\'assistant IA...', askAi: 'Interroger l\'assistant IA...',
toolLaunched: 'Outil lanc\u00e9',
}, },
config: { config: {

View File

@@ -391,6 +391,10 @@ input::placeholder { color: var(--text-disabled); }
.ai-message { padding: 8px 12px; border-radius: var(--radius); font-size: 13px; line-height: 1.5; word-break: break-word; } .ai-message { padding: 8px 12px; border-radius: var(--radius); font-size: 13px; line-height: 1.5; word-break: break-word; }
.ai-message.ai { background: var(--bg-card); border-left: 3px solid var(--accent); } .ai-message.ai { background: var(--bg-card); border-left: 3px solid var(--accent); }
.ai-message.user { background: var(--accent-bg); border-left: 3px solid var(--accent-muted); } .ai-message.user { background: var(--accent-bg); border-left: 3px solid var(--accent-muted); }
.ai-message.thinking { background: var(--bg-elevated); border-left: 3px solid var(--info); font-style: italic; color: var(--text-tertiary); }
.ai-message.tool { background: var(--bg-elevated); border-left: 3px solid var(--warning); }
.ai-message.tool .tool-name { font-weight: 700; color: var(--warning); }
.ai-message.tool .tool-args { font-family: var(--font-mono); font-size: 12px; color: var(--text-tertiary); margin-top: 4px; }
.ai-panel-input { display: flex; gap: 6px; padding: 10px 12px; border-top: 1px solid var(--border); } .ai-panel-input { display: flex; gap: 6px; padding: 10px 12px; border-top: 1px solid var(--border); }
.ai-panel-input input { flex: 1; font-size: 13px; padding: 6px 10px; } .ai-panel-input input { flex: 1; font-size: 13px; padding: 6px 10px; }