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:
@@ -26,6 +26,17 @@ const api = {
|
||||
runScan: () => request('/scan', { method: 'POST' }),
|
||||
installTools: (tools) => request('/install', { method: 'POST', body: JSON.stringify({ tools }) }),
|
||||
configureMCP: () => request('/mcp/configure', { method: 'POST' }),
|
||||
configureMCPForEditor: (editor) => request('/mcp/configure', { method: 'POST', body: JSON.stringify({ editor }) }),
|
||||
getMCPStatus: () => request('/mcp/status'),
|
||||
getMCPRegistry: () => request('/mcp/registry'),
|
||||
getLSPHealth: () => request('/lsp/health'),
|
||||
autoInstallLSP: (projectDir) => request('/lsp/auto-install', { method: 'POST', body: JSON.stringify({ project_dir: projectDir || '' }) }),
|
||||
generateLSPConfig: (editor, names) => request('/lsp/editor-config', { method: 'POST', body: JSON.stringify({ editor, names }) }),
|
||||
validateSkill: (name) => request('/skills/validate', { method: 'POST', body: JSON.stringify({ name }) }),
|
||||
testSkill: (name, sampleTask) => request('/skills/test', { method: 'POST', body: JSON.stringify({ name, sample_task: sampleTask || '' }) }),
|
||||
exportSkill: (name) => request('/skills/export', { method: 'POST', body: JSON.stringify({ name }) }),
|
||||
importSkill: (path) => request('/skills/import', { method: 'POST', body: JSON.stringify({ import_path: path }) }),
|
||||
getDashboardStatus: () => request('/dashboard/status'),
|
||||
savePreferences: (prefs) => request('/preferences', { method: 'PUT', body: JSON.stringify(prefs) }),
|
||||
saveProfile: (profile) => request('/config/profile', { method: 'PUT', body: JSON.stringify(profile) }),
|
||||
saveProvider: (provider) => request('/config/provider', { method: 'PUT', body: JSON.stringify(provider) }),
|
||||
@@ -84,6 +95,66 @@ const api = {
|
||||
}).catch(reject)
|
||||
})
|
||||
},
|
||||
sendShellChat: (message, context = {}, stream = true, onChunk) => {
|
||||
const payload = {
|
||||
message,
|
||||
context: context.context || '',
|
||||
history: context.history || [],
|
||||
cwd: context.cwd || '',
|
||||
platform: context.platform || '',
|
||||
stream,
|
||||
}
|
||||
if (!stream) {
|
||||
return request('/shell/chat', { method: 'POST', body: JSON.stringify(payload) })
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
fetch(`${API_BASE}/shell/chat`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
}).then(async (res) => {
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({ error: res.statusText }))
|
||||
reject(new Error(err.error || res.statusText))
|
||||
return
|
||||
}
|
||||
const reader = res.body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let full = ''
|
||||
let toolCalls = []
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
const text = decoder.decode(value, { stream: true })
|
||||
for (const line of text.split('\n')) {
|
||||
if (!line.startsWith('data: ')) continue
|
||||
try {
|
||||
const data = JSON.parse(line.slice(6))
|
||||
if (data.error) { reject(new Error(data.error)); return }
|
||||
if (data.done) {
|
||||
resolve({ content: full, tool_calls: toolCalls })
|
||||
return
|
||||
}
|
||||
if (data.content) {
|
||||
full += data.content
|
||||
if (onChunk) onChunk(full, data)
|
||||
} else if (data.tool_call) {
|
||||
toolCalls.push(data.tool_call)
|
||||
if (onChunk) onChunk(full, data, toolCalls)
|
||||
} else if (data.tool_result) {
|
||||
const idx = toolCalls.findIndex(tc => tc.tool_call_id === data.tool_result.id)
|
||||
if (idx >= 0) {
|
||||
toolCalls[idx].result = data.tool_result
|
||||
}
|
||||
if (onChunk) onChunk(full, data, toolCalls)
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
resolve({ content: full, tool_calls: toolCalls })
|
||||
}).catch(reject)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default api
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -22,7 +22,9 @@ const en = {
|
||||
|
||||
dashboard: {
|
||||
systemOverview: 'System Overview',
|
||||
tools: 'tools',
|
||||
tools: 'Tools',
|
||||
activity: 'Activity',
|
||||
toolsCount: '{count} tools installed',
|
||||
installed: 'Installed',
|
||||
missing: 'Missing',
|
||||
quickActions: 'Quick Actions',
|
||||
@@ -39,9 +41,20 @@ const en = {
|
||||
installStarted: 'Install started. Rescanning...',
|
||||
done: 'Done.',
|
||||
scanComplete: 'Scan complete.',
|
||||
scanFailed: 'Scan failed',
|
||||
updatesCount: '{count} updates available.',
|
||||
allUpToDate: 'All tools up to date.',
|
||||
mcpConfigured: 'MCP configured.',
|
||||
mcpConfigFailed: 'MCP configuration failed',
|
||||
status: 'Status',
|
||||
clearLog: 'Clear',
|
||||
noActivity: 'No recent activity.',
|
||||
rescanning: 'Scanning...',
|
||||
install: 'Install',
|
||||
installFailed: 'Install failed',
|
||||
checkUpdatesFailed: 'Check failed',
|
||||
configuringMCP: 'Configuring MCP...',
|
||||
mcpConfigFailed: 'MCP configuration failed',
|
||||
},
|
||||
|
||||
studio: {
|
||||
@@ -111,6 +124,7 @@ const en = {
|
||||
aiWelcome: 'Hello! I can help you with terminal commands. Ask me anything!',
|
||||
askAi: 'Ask AI assistant...',
|
||||
toolLaunched: 'Tool launched',
|
||||
toolResult: 'Result',
|
||||
},
|
||||
|
||||
config: {
|
||||
|
||||
@@ -22,7 +22,9 @@ const fr = {
|
||||
|
||||
dashboard: {
|
||||
systemOverview: 'Vue d\u2019ensemble du syst\u00e8me',
|
||||
tools: 'outils',
|
||||
tools: 'Outils',
|
||||
activity: 'Activit\u00e9',
|
||||
toolsCount: '{count} outils install\u00e9s',
|
||||
installed: 'Install\u00e9',
|
||||
missing: 'Manquant',
|
||||
quickActions: 'Actions rapides',
|
||||
@@ -39,9 +41,20 @@ const fr = {
|
||||
installStarted: 'Installation lanc\u00e9e. Rescan en cours...',
|
||||
done: 'Termin\u00e9.',
|
||||
scanComplete: 'Scan termin\u00e9.',
|
||||
scanFailed: '\u00c9chec du scan',
|
||||
updatesCount: '{count} mises \u00e0 jour disponibles.',
|
||||
allUpToDate: 'Tous les outils sont \u00e0 jour.',
|
||||
mcpConfigured: 'MCP configur\u00e9.',
|
||||
status: 'Statut',
|
||||
noTools: 'Aucun outil d\u00e9tect\u00e9. Ex\u00e9cutez un scan.',
|
||||
clearLog: 'Effacer',
|
||||
noActivity: 'Aucune activit\u00e9 r\u00e9cente.',
|
||||
rescanning: 'Scan en cours...',
|
||||
install: 'Installer',
|
||||
installFailed: '\u00c9chec de l\u2019installation',
|
||||
checkUpdatesFailed: '\u00c9chec de la v\u00e9rification',
|
||||
configuringMCP: 'Configuration MCP en cours...',
|
||||
mcpConfigFailed: '\u00c9chec de la configuration MCP',
|
||||
},
|
||||
|
||||
studio: {
|
||||
@@ -111,6 +124,7 @@ const fr = {
|
||||
aiWelcome: 'Bonjour ! Je peux vous aider avec les commandes du terminal. Demandez-moi n\'importe quoi !',
|
||||
askAi: 'Interroger l\'assistant IA...',
|
||||
toolLaunched: 'Outil lanc\u00e9',
|
||||
toolResult: 'R\u00e9sultat',
|
||||
},
|
||||
|
||||
config: {
|
||||
|
||||
@@ -565,6 +565,81 @@ input::placeholder { color: var(--text-disabled); }
|
||||
letter-spacing: 1px; margin-bottom: 12px; padding-bottom: 6px; border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* ── Dashboard Tabs ── */
|
||||
.dashboard-tabs {
|
||||
display: flex; gap: 4px; padding: 12px 20px 0;
|
||||
border-bottom: 1px solid var(--border); background: var(--bg-surface); flex-shrink: 0;
|
||||
}
|
||||
.dashboard-tab {
|
||||
padding: 8px 16px; border-radius: var(--radius) var(--radius) 0 0;
|
||||
border: 1px solid transparent; border-bottom: none; background: transparent;
|
||||
color: var(--text-tertiary); font-size: 12px; font-weight: 600; cursor: pointer;
|
||||
display: flex; align-items: center; gap: 6px; transition: all 0.15s;
|
||||
}
|
||||
.dashboard-tab:hover { color: var(--text-primary); background: var(--bg-hover); }
|
||||
.dashboard-tab.active { background: var(--bg-card); color: var(--accent); border-color: var(--border); }
|
||||
.dashboard-tab .tab-icon { font-size: 14px; }
|
||||
.dashboard-tab .tab-count {
|
||||
background: var(--bg-input); padding: 1px 6px; border-radius: 10px; font-size: 10px; font-family: var(--font-mono);
|
||||
}
|
||||
.dashboard-tab .tab-count.warn { background: var(--accent-bg); color: var(--accent); }
|
||||
|
||||
.dashboard-tools-panel { padding: 20px 24px; }
|
||||
.dashboard-tools-stats { display: flex; gap: 12px; font-size: 12px; }
|
||||
.stat-ok { color: var(--success); font-family: var(--font-mono); }
|
||||
.stat-missing { color: var(--error); font-family: var(--font-mono); }
|
||||
|
||||
.dashboard-system-info { display: flex; gap: 8px; align-items: center; margin-bottom: 16px; font-size: 12px; color: var(--text-tertiary); }
|
||||
.sys-info-item { font-family: var(--font-mono); }
|
||||
.sys-info-sep { color: var(--text-disabled); }
|
||||
|
||||
.tools-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px; margin-top: 8px; }
|
||||
.tool-card {
|
||||
background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius-lg);
|
||||
padding: 14px 16px; display: flex; align-items: center; gap: 12px; transition: border-color 0.2s;
|
||||
}
|
||||
.tool-card:hover { border-color: var(--accent-dim); }
|
||||
.tool-card.installed { border-left: 3px solid var(--success); }
|
||||
.tool-card.missing { border-left: 3px solid var(--error); }
|
||||
.tool-card-icon { font-size: 20px; flex-shrink: 0; }
|
||||
.tool-card-info { flex: 1; min-width: 0; }
|
||||
.tool-card-name { font-weight: 600; font-size: 13px; color: var(--text-primary); margin-bottom: 2px; }
|
||||
.tool-card-version { font-size: 11px; color: var(--text-tertiary); display: flex; align-items: center; gap: 6px; }
|
||||
.tool-version-text { font-family: var(--font-mono); font-size: 10px; color: var(--text-disabled); }
|
||||
.status-ok { color: var(--success); }
|
||||
.status-missing { color: var(--error); }
|
||||
.tool-card-actions { flex-shrink: 0; display: flex; align-items: center; gap: 6px; }
|
||||
.tool-update-badge { background: var(--accent-bg); color: var(--accent); font-size: 10px; font-family: var(--font-mono); padding: 2px 6px; border-radius: 4px; cursor: pointer; }
|
||||
.tool-update-badge:hover { background: var(--accent-dim); }
|
||||
|
||||
.dashboard-activity-panel { padding: 20px 24px; }
|
||||
.activity-log { display: flex; flex-direction: column; gap: 2px; }
|
||||
.notif-icon { font-size: 12px; width: 16px; text-align: center; }
|
||||
|
||||
.dashboard-actions-panel { padding: 20px 24px; }
|
||||
.quick-actions-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 10px; margin-bottom: 24px; }
|
||||
.quick-action-btn {
|
||||
background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius-lg);
|
||||
padding: 16px 20px; display: flex; align-items: center; gap: 12px; cursor: pointer;
|
||||
transition: all 0.2s; font-size: 13px; color: var(--text-secondary);
|
||||
}
|
||||
.quick-action-btn:hover:not(:disabled) { border-color: var(--accent-dim); background: var(--bg-hover); color: var(--text-primary); }
|
||||
.quick-action-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.quick-action-icon { font-size: 18px; }
|
||||
.quick-action-label { font-weight: 600; }
|
||||
|
||||
.dashboard-updates-section { margin-top: 16px; }
|
||||
.updates-list { display: flex; flex-direction: column; gap: 6px; }
|
||||
.update-row {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 10px 14px; border-radius: var(--radius); background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.update-row:hover { border-color: var(--accent-dim); }
|
||||
.update-info { display: flex; align-items: center; gap: 16px; }
|
||||
.update-name { font-weight: 600; color: var(--text-primary); font-size: 13px; min-width: 100px; }
|
||||
.update-versions { color: var(--text-tertiary); font-size: 12px; font-family: var(--font-mono); }
|
||||
|
||||
.panel-header {
|
||||
display: flex; align-items: center; justify-content: space-between; padding: 10px 16px;
|
||||
border-bottom: 1px solid var(--border); background: var(--bg-surface);
|
||||
|
||||
Reference in New Issue
Block a user