feat: add Cobra CLI, LSP/MCP registries, workflow engine, and enriched dashboard
All checks were successful
Beta Release / beta (push) Successful in 2m24s
All checks were successful
Beta Release / beta (push) Successful in 2m24s
Major changes: - Refactor CLI entry point to Cobra commands (root, setup, scan, doctor, install, update, lsp, mcp, skills, config, version) - Add LSP registry with health checks, auto-install, and editor config generation - Add MCP registry with editor detection, status tracking, and per-editor configuration - Add workflow engine with planner and step execution for automated task chains - Add conversation search, export (Markdown/JSON), and detailed token counting - Add streaming shell chat handler with tool call/result events - Add skill validation, dry-run testing, and export endpoints - Enrich dashboard with Tools/Activity/Status tabs and tool cards grid - Add PRD documentation - Complete i18n for both EN and FR 💘 Generated with Crush Assisted-by: GLM-5.1 via Crush <crush@charm.land>
This commit is contained in:
@@ -447,7 +447,14 @@ function PanelSkills({ skillList, t }) {
|
||||
<div key={i} className="config-skill-row">
|
||||
<span className="config-skill-name">{s.name}</span>
|
||||
<span className="badge neutral">{s.target || 'both'}</span>
|
||||
{s.version && <span className="badge" style={{ fontSize: 10 }}>{s.version}</span>}
|
||||
{s.category && <span className="badge" style={{ fontSize: 10, opacity: 0.7 }}>{s.category}</span>}
|
||||
<span className="config-skill-desc">{s.description}</span>
|
||||
{s.dependencies && s.dependencies.length > 0 && (
|
||||
<div style={{ marginTop: 4, fontSize: 10, color: 'var(--muted)' }}>
|
||||
deps: {s.dependencies.map(d => d.name).join(', ')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
|
||||
@@ -1,58 +1,438 @@
|
||||
import { useState } from 'react'
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useI18n } from '../i18n'
|
||||
|
||||
export default function Dashboard({ api }) {
|
||||
const TOOL_ICONS = {
|
||||
crush: '⚡',
|
||||
claude: '🤖',
|
||||
go: '🔷',
|
||||
node: '🟢',
|
||||
python: '🐍',
|
||||
docker: '🐳',
|
||||
git: '📚',
|
||||
ssh: '🌐',
|
||||
starship: '🚀',
|
||||
rust: '🦀',
|
||||
}
|
||||
|
||||
function ToolCard({ tool, onInstall, installing }) {
|
||||
const { t } = useI18n()
|
||||
const [notifications, setNotifications] = useState([])
|
||||
const [showInstall, setShowInstall] = useState(false)
|
||||
|
||||
const icon = TOOL_ICONS[tool.name?.toLowerCase()] || '🔧'
|
||||
const isInstalled = tool.installed || tool.status === 'installed'
|
||||
const version = tool.version || ''
|
||||
const hasUpdate = tool.hasUpdate || tool.updateAvailable
|
||||
|
||||
return (
|
||||
<div className="dashboard-layout">
|
||||
<div className="dashboard-content">
|
||||
<div className="dashboard-grid">
|
||||
<div className="dashboard-section">
|
||||
<div className="dashboard-section-header">
|
||||
<div className="dashboard-section-title">{t('studio.workflows')}</div>
|
||||
</div>
|
||||
<div className="dashboard-workflows-inline">
|
||||
<div className="workflow-section">
|
||||
<div className="section-label">{t('studio.workflows')}</div>
|
||||
<div className="empty-state" style={{ padding: 20 }}>
|
||||
{t('studio.noWorkflow')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="workflow-section">
|
||||
<div className="section-label">{t('studio.activeAgents')}</div>
|
||||
<div className="empty-state" style={{ padding: 20 }}>
|
||||
{t('studio.noWorkflow')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="dashboard-section">
|
||||
<div className="dashboard-section-header">
|
||||
<div className="dashboard-section-title">{t('dashboard.activityLog')}</div>
|
||||
{notifications.length > 0 && (
|
||||
<span className="badge warn">{notifications.length}</span>
|
||||
)}
|
||||
</div>
|
||||
{notifications.length === 0 ? (
|
||||
<div className="empty-state">{t('dashboard.noUpdateData')}</div>
|
||||
) : (
|
||||
<div className="dashboard-notifications-inline">
|
||||
{notifications.map(n => (
|
||||
<div key={n.id} className={`notif-row notif-${n.type}`}>
|
||||
<span className="notif-time">
|
||||
{n.time.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', second: '2-digit' })}
|
||||
</span>
|
||||
<span className="notif-text">{n.text}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={`tool-card ${isInstalled ? 'installed' : 'missing'}`}>
|
||||
<div className="tool-card-icon">{icon}</div>
|
||||
<div className="tool-card-info">
|
||||
<div className="tool-card-name">{tool.name || 'Unknown'}</div>
|
||||
<div className="tool-card-version">
|
||||
{isInstalled ? (
|
||||
<span className="status-ok">{t('dashboard.installed')}</span>
|
||||
) : (
|
||||
<span className="status-missing">{t('dashboard.missing')}</span>
|
||||
)}
|
||||
{version && <span className="tool-version-text">{version}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="tool-card-actions">
|
||||
{isInstalled && hasUpdate && (
|
||||
<span className="tool-update-badge" title={`Update to ${tool.latestVersion || 'latest'}`}>
|
||||
↑ {tool.latestVersion || 'new'}
|
||||
</span>
|
||||
)}
|
||||
{!isInstalled && (
|
||||
<button
|
||||
className="sm primary"
|
||||
onClick={() => onInstall(tool.name)}
|
||||
disabled={installing}
|
||||
>
|
||||
{installing ? '...' : t('dashboard.install')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ActivityItem({ entry }) {
|
||||
const time = entry.time
|
||||
? new Date(entry.time).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', second: '2-digit' })
|
||||
: ''
|
||||
const type = entry.type || entry.level || 'info'
|
||||
const text = entry.message || entry.text || entry.content || ''
|
||||
|
||||
const typeClass = {
|
||||
ok: 'notif-ok',
|
||||
success: 'notif-ok',
|
||||
install: 'notif-ok',
|
||||
update: 'notif-info',
|
||||
info: 'notif-info',
|
||||
warn: 'notif-warn',
|
||||
warning: 'notif-warn',
|
||||
error: 'notif-error',
|
||||
fail: 'notif-error',
|
||||
}[type] || 'notif-info'
|
||||
|
||||
const icon = {
|
||||
ok: '✓', success: '✓', install: '✓', update: '→',
|
||||
info: 'ℹ', warn: '⚠', warning: '⚠', error: '✗', fail: '✗',
|
||||
}[type] || '•'
|
||||
|
||||
return (
|
||||
<div className={`notif-row ${typeClass}`}>
|
||||
<span className="notif-time">{time}</span>
|
||||
<span className="notif-icon">{icon}</span>
|
||||
<span className="notif-text">{text}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function QuickActionButton({ icon, label, onClick, loading, disabled }) {
|
||||
return (
|
||||
<button
|
||||
className="quick-action-btn"
|
||||
onClick={onClick}
|
||||
disabled={disabled || loading}
|
||||
>
|
||||
{loading ? <span className="spinner" style={{ width: 14, height: 14 }} /> : <span className="quick-action-icon">{icon}</span>}
|
||||
<span className="quick-action-label">{label}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Dashboard({ api }) {
|
||||
const { t } = useI18n()
|
||||
const [activeTab, setActiveTab] = useState('tools')
|
||||
const [tools, setTools] = useState([])
|
||||
const [updates, setUpdates] = useState([])
|
||||
const [systemInfo, setSystemInfo] = useState(null)
|
||||
const [notifications, setNotifications] = useState([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [installing, setInstalling] = useState(false)
|
||||
const [scanLoading, setScanLoading] = useState(false)
|
||||
const [mcpLoading, setMcpLoading] = useState(false)
|
||||
const [dashboardStatus, setDashboardStatus] = useState(null)
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
try {
|
||||
const [toolsData, updatesData, systemData] = await Promise.all([
|
||||
api.getTools().catch(() => ({ tools: [] })),
|
||||
api.getUpdates().catch(() => ({ updates: [] })),
|
||||
api.getSystem().catch(() => null),
|
||||
])
|
||||
setTools(toolsData.tools || toolsData || [])
|
||||
setUpdates(updatesData.updates || updatesData || [])
|
||||
setSystemInfo(systemData)
|
||||
api.getDashboardStatus().then(d => setDashboardStatus(d)).catch(() => {})
|
||||
} catch (err) {
|
||||
console.error('Failed to load dashboard data:', err)
|
||||
}
|
||||
}, [api])
|
||||
|
||||
useEffect(() => {
|
||||
loadData()
|
||||
}, [loadData])
|
||||
|
||||
const addNotification = (message, type = 'info') => {
|
||||
const entry = { id: Date.now(), time: new Date().toISOString(), message, type }
|
||||
setNotifications(prev => [entry, ...prev].slice(0, 100))
|
||||
}
|
||||
|
||||
const handleRescan = async () => {
|
||||
setScanLoading(true)
|
||||
addNotification(t('dashboard.rescanning'), 'info')
|
||||
try {
|
||||
await api.runScan()
|
||||
await loadData()
|
||||
addNotification(t('dashboard.scanComplete'), 'ok')
|
||||
} catch (err) {
|
||||
addNotification(`${t('dashboard.scanFailed')}: ${err.message}`, 'error')
|
||||
} finally {
|
||||
setScanLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleInstallMissing = async () => {
|
||||
const missing = tools.filter(t => !t.installed && t.status !== 'installed')
|
||||
if (missing.length === 0) return
|
||||
setInstalling(true)
|
||||
addNotification(t('dashboard.installing', { count: missing.length }), 'info')
|
||||
try {
|
||||
await api.installTools(missing.map(t => t.name))
|
||||
addNotification(t('dashboard.installStarted'), 'ok')
|
||||
setTimeout(() => handleRescan(), 2000)
|
||||
} catch (err) {
|
||||
addNotification(`${t('dashboard.installFailed')}: ${err.message}`, 'error')
|
||||
} finally {
|
||||
setInstalling(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCheckUpdates = async () => {
|
||||
setLoading(true)
|
||||
addNotification(t('config.checking'), 'info')
|
||||
try {
|
||||
const data = await api.getUpdates()
|
||||
setUpdates(data.updates || data || [])
|
||||
const count = (data.updates || data || []).length
|
||||
if (count > 0) {
|
||||
addNotification(t('dashboard.updatesCount', { count }), 'warn')
|
||||
} else {
|
||||
addNotification(t('dashboard.allUpToDate'), 'ok')
|
||||
}
|
||||
} catch (err) {
|
||||
addNotification(`${t('dashboard.checkUpdatesFailed')}: ${err.message}`, 'error')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleConfigureMCP = async () => {
|
||||
setMcpLoading(true)
|
||||
addNotification(t('dashboard.configuringMCP'), 'info')
|
||||
try {
|
||||
await api.configureMCP()
|
||||
addNotification(t('dashboard.mcpConfigured'), 'ok')
|
||||
} catch (err) {
|
||||
addNotification(`${t('dashboard.mcpConfigFailed')}: ${err.message}`, 'error')
|
||||
} finally {
|
||||
setMcpLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleInstallTool = async (name) => {
|
||||
setInstalling(true)
|
||||
addNotification(`${t('dashboard.installing')} ${name}...`, 'info')
|
||||
try {
|
||||
await api.installTools([name])
|
||||
addNotification(`${name} ${t('dashboard.installed')}`, 'ok')
|
||||
setTimeout(() => loadData(), 2000)
|
||||
} catch (err) {
|
||||
addNotification(`${t('dashboard.installFailed')}: ${err.message}`, 'error')
|
||||
} finally {
|
||||
setInstalling(false)
|
||||
}
|
||||
}
|
||||
|
||||
const installedCount = tools.filter(t => t.installed || t.status === 'installed').length
|
||||
const missingCount = tools.length - installedCount
|
||||
|
||||
return (
|
||||
<div className="dashboard-layout">
|
||||
<div className="dashboard-tabs">
|
||||
<button
|
||||
className={`dashboard-tab ${activeTab === 'tools' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('tools')}
|
||||
>
|
||||
<span className="tab-icon">🔧</span>
|
||||
{t('dashboard.tools')}
|
||||
<span className="tab-count">{installedCount}</span>
|
||||
</button>
|
||||
<button
|
||||
className={`dashboard-tab ${activeTab === 'activity' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('activity')}
|
||||
>
|
||||
<span className="tab-icon">📋</span>
|
||||
{t('dashboard.activity')}
|
||||
{notifications.length > 0 && <span className="tab-count warn">{notifications.length}</span>}
|
||||
</button>
|
||||
<button
|
||||
className={`dashboard-tab ${activeTab === 'actions' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('actions')}
|
||||
>
|
||||
<span className="tab-icon">⚡</span>
|
||||
{t('dashboard.quickActions')}
|
||||
</button>
|
||||
<button
|
||||
className={`dashboard-tab ${activeTab === 'status' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('status')}
|
||||
>
|
||||
<span className="tab-icon">📡</span>
|
||||
{t('dashboard.status') || 'Status'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="dashboard-content">
|
||||
{activeTab === 'tools' && (
|
||||
<div className="dashboard-tools-panel">
|
||||
<div className="dashboard-section-header">
|
||||
<div className="dashboard-section-title">{t('dashboard.systemOverview')}</div>
|
||||
<div className="dashboard-tools-stats">
|
||||
<span className="stat-ok">{installedCount} {t('dashboard.installed')}</span>
|
||||
{missingCount > 0 && <span className="stat-missing">{missingCount} {t('dashboard.missing')}</span>}
|
||||
</div>
|
||||
</div>
|
||||
{systemInfo && (
|
||||
<div className="dashboard-system-info">
|
||||
<span className="sys-info-item">{systemInfo.os || systemInfo.platform || 'Unknown'}</span>
|
||||
<span className="sys-info-sep">·</span>
|
||||
<span className="sys-info-item">{systemInfo.arch || 'Unknown'}</span>
|
||||
{systemInfo.shell && <><span className="sys-info-sep">·</span><span className="sys-info-item">{systemInfo.shell}</span></>}
|
||||
</div>
|
||||
)}
|
||||
<div className="tools-grid">
|
||||
{tools.length === 0 && (
|
||||
<div className="empty-state">{t('dashboard.noTools')}</div>
|
||||
)}
|
||||
{tools.map((tool, i) => (
|
||||
<ToolCard
|
||||
key={tool.name || i}
|
||||
tool={tool}
|
||||
onInstall={handleInstallTool}
|
||||
installing={installing}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'activity' && (
|
||||
<div className="dashboard-activity-panel">
|
||||
<div className="dashboard-section-header">
|
||||
<div className="dashboard-section-title">{t('dashboard.activityLog')}</div>
|
||||
<button className="sm ghost" onClick={() => setNotifications([])} disabled={notifications.length === 0}>
|
||||
{t('dashboard.clearLog')}
|
||||
</button>
|
||||
</div>
|
||||
{notifications.length === 0 ? (
|
||||
<div className="empty-state">{t('dashboard.noActivity')}</div>
|
||||
) : (
|
||||
<div className="activity-log">
|
||||
{notifications.map(entry => (
|
||||
<ActivityItem key={entry.id} entry={entry} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'actions' && (
|
||||
<div className="dashboard-actions-panel">
|
||||
<div className="dashboard-section-header">
|
||||
<div className="dashboard-section-title">{t('dashboard.quickActions')}</div>
|
||||
</div>
|
||||
<div className="quick-actions-grid">
|
||||
<QuickActionButton
|
||||
icon="🔍"
|
||||
label={t('dashboard.rescanSystem')}
|
||||
onClick={handleRescan}
|
||||
loading={scanLoading}
|
||||
/>
|
||||
<QuickActionButton
|
||||
icon="📦"
|
||||
label={t('dashboard.installMissing')}
|
||||
onClick={handleInstallMissing}
|
||||
loading={installing}
|
||||
disabled={missingCount === 0}
|
||||
/>
|
||||
<QuickActionButton
|
||||
icon="🔄"
|
||||
label={t('dashboard.checkUpdates')}
|
||||
onClick={handleCheckUpdates}
|
||||
loading={loading}
|
||||
/>
|
||||
<QuickActionButton
|
||||
icon="⚙"
|
||||
label={t('dashboard.configureMCP')}
|
||||
onClick={handleConfigureMCP}
|
||||
loading={mcpLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{updates.length > 0 && (
|
||||
<div className="dashboard-updates-section">
|
||||
<div className="dashboard-section-header">
|
||||
<div className="dashboard-section-title">{t('dashboard.updates')}</div>
|
||||
<span className="badge warn">{updates.length}</span>
|
||||
</div>
|
||||
<div className="updates-list">
|
||||
{updates.map((update, i) => (
|
||||
<div key={update.name || i} className="update-row">
|
||||
<div className="update-info">
|
||||
<span className="update-name">{update.name || 'Unknown'}</span>
|
||||
<span className="update-versions">
|
||||
{update.current || update.version || '?'} → {update.latest || update.target || '?'}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
className="sm"
|
||||
onClick={() => api.runUpdate(update.name)}
|
||||
disabled={loading}
|
||||
>
|
||||
{t('dashboard.update')}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'status' && (
|
||||
<div className="dashboard-status-panel">
|
||||
{dashboardStatus ? (
|
||||
<>
|
||||
<div className="dashboard-section-header">
|
||||
<div className="dashboard-section-title">MCP Servers</div>
|
||||
<span className="badge">{dashboardStatus.mcp?.healthy || 0}/{dashboardStatus.mcp?.total || 0} healthy</span>
|
||||
</div>
|
||||
<div className="tools-grid" style={{ marginBottom: 16 }}>
|
||||
{(dashboardStatus.mcp?.servers || []).map((s, i) => (
|
||||
<div key={i} className={`tool-card ${s.healthy ? 'installed' : s.installed ? '' : 'missing'}`}>
|
||||
<div className="tool-card-info">
|
||||
<div className="tool-card-name">{s.name}</div>
|
||||
<div className="tool-card-version">
|
||||
{s.healthy ? <span className="status-ok">healthy</span> :
|
||||
s.installed ? <span className="status-missing">installed</span> :
|
||||
<span className="status-missing">not found</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="dashboard-section-header">
|
||||
<div className="dashboard-section-title">LSP Servers</div>
|
||||
<span className="badge">{dashboardStatus.lsp?.installed || 0}/{dashboardStatus.lsp?.total || 0} installed</span>
|
||||
</div>
|
||||
<div className="tools-grid" style={{ marginBottom: 16 }}>
|
||||
{(dashboardStatus.lsp?.servers || []).filter(s => s.installed).map((s, i) => (
|
||||
<div key={i} className="tool-card installed">
|
||||
<div className="tool-card-info">
|
||||
<div className="tool-card-name">{s.name}</div>
|
||||
<div className="tool-card-version">
|
||||
<span className="status-ok">{s.language}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="dashboard-section-header">
|
||||
<div className="dashboard-section-title">Skills</div>
|
||||
<span className="badge">{dashboardStatus.skills?.total || 0} deployed</span>
|
||||
{(dashboardStatus.skills?.issues || []).length > 0 && (
|
||||
<span className="badge warn">{(dashboardStatus.skills.issues || []).length} issues</span>
|
||||
)}
|
||||
</div>
|
||||
{(dashboardStatus.skills?.issues || []).length > 0 && (
|
||||
<div style={{ fontSize: 12, color: 'var(--warning)', marginBottom: 8 }}>
|
||||
{(dashboardStatus.skills.issues || []).map((issue, i) => (
|
||||
<div key={i}>{issue}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="empty-state">Loading status...</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -378,61 +378,49 @@ export default function Shell({ api }) {
|
||||
setAiMessages(prev => [...prev, { role: 'user', content: text }])
|
||||
setAiInput('')
|
||||
setAiLoading(true)
|
||||
|
||||
const currentTab = tabs.find(t => t.id === activeTab)
|
||||
const context = {
|
||||
cwd: currentTab?.cwd || '',
|
||||
platform: navigator.platform || '',
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await api.runCommand(`echo "AI: ${text}"`, '')
|
||||
const output = res.output || t('shell.noResponse')
|
||||
parseAndAddAiMessages(output)
|
||||
} catch (err) {
|
||||
setAiMessages(prev => [...prev, { role: 'ai', content: `${t('shell.error')}: ${err.message}` }])
|
||||
}
|
||||
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))
|
||||
let accumulated = ''
|
||||
await api.sendShellChat(text, context, true, (partial, event) => {
|
||||
if (event && event.tool_call) {
|
||||
setAiMessages(prev => [...prev, {
|
||||
role: 'tool',
|
||||
content: `${t('shell.toolLaunched')}: ${toolData.tool || 'tool'}`,
|
||||
args: toolData.task || toolData.args || '',
|
||||
content: `${t('shell.toolLaunched')}: ${event.tool_call.name || 'tool'}`,
|
||||
args: event.tool_call.args ? JSON.stringify(event.tool_call.args).slice(0, 100) : '',
|
||||
}])
|
||||
} catch {
|
||||
setAiMessages(prev => [...prev, { role: 'tool', content: line, args: '' }])
|
||||
return
|
||||
}
|
||||
} else if (line.match(/^(Reflexion|Thought|thinking):/i) || line.startsWith('>')) {
|
||||
if (buffer.trim() && !inBlock) {
|
||||
flushBuffer()
|
||||
if (event && event.tool_result) {
|
||||
const resultText = event.tool_result.result?.content || event.tool_result.error || 'completed'
|
||||
setAiMessages(prev => [...prev, {
|
||||
role: 'tool_result',
|
||||
content: resultText,
|
||||
isError: event.tool_result.result?.is_error,
|
||||
}])
|
||||
return
|
||||
}
|
||||
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
|
||||
if (event && event.done) return
|
||||
accumulated = partial
|
||||
setAiMessages(prev => {
|
||||
const filtered = prev.filter(m => !m._streaming)
|
||||
return [...filtered, { role: 'ai', content: partial, _streaming: true }]
|
||||
})
|
||||
})
|
||||
|
||||
setAiMessages(prev => prev.filter(m => !m._streaming))
|
||||
if (accumulated) {
|
||||
setAiMessages(prev => [...prev.filter(m => !m._streaming), { role: 'ai', content: accumulated }])
|
||||
}
|
||||
} catch (err) {
|
||||
setAiMessages(prev => [...prev.filter(m => !m._streaming), { role: 'ai', content: `${t('shell.error')}: ${err.message}` }])
|
||||
}
|
||||
flushBuffer()
|
||||
setAiLoading(false)
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user