feat(shell): dedicated System Analyst AI, no code execution, analyze system
All checks were successful
Beta Release / beta (push) Successful in 45s

- New ShellConvStore with persistent history (shell_conversation.json)
- 100k token limit — input grays out, must /clear to continue
- Commands limited to /clear and /help only
- Shell AI has NO tools — read-only analysis, never executes code
- "Analyste Système" panel with system analysis button
- System analysis uses Studio AI to write system_analysis.md,
  prepended as context on every conversation start
- Code blocks show "Copier" and "Terminal" buttons to copy or
  send code directly to the active terminal via WebSocket
- Token bar shows usage with warning at 80%

💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
This commit is contained in:
Augustin
2026-04-23 21:50:06 +02:00
parent eda7293286
commit f9c4cf11ff
9 changed files with 634 additions and 221 deletions

View File

@@ -1,4 +1,4 @@
import { useState, useRef, useEffect, useCallback } from 'react'
import { useState, useRef, useEffect, useCallback, useMemo } from 'react'
import { useI18n } from '../i18n'
const RANKS = {
@@ -76,7 +76,7 @@ function formatText(text) {
return html
}
function ThinkingBlock({ content, done }) {
function ThinkingBlock({ content, done, raw }) {
return (
<div className={`feed-thinking-block ${done ? 'done' : 'active'}`}>
<div className="feed-thinking-header">
@@ -86,7 +86,9 @@ function ThinkingBlock({ content, done }) {
<span>Reflexion</span>
{!done && <span className="feed-thinking-dots"><span/><span/><span/></span>}
</div>
<div className="feed-thinking-content">{content}</div>
<div className="feed-thinking-content">
{raw ? <span dangerouslySetInnerHTML={{ __html: content }} /> : content}
</div>
</div>
)
}
@@ -200,7 +202,7 @@ function FeedItem({ msg }) {
<span className="feed-role">{rank.label}</span>
{timeStr && <span className="feed-time">{timeStr}</span>}
</div>
{msg.thinking && <ThinkingBlock content={msg.thinking} done />}
{msg.thinking && <ThinkingBlock content={formatText(msg.thinking)} done raw />}
{parsedToolCalls && parsedToolCalls.map((tc, i) => {
const resultData = parsedToolResults
? parsedToolResults.find(r => r.tool_call_id === tc.tool_call_id)
@@ -234,6 +236,16 @@ function StreamingItem({ content, thinking, toolCalls }) {
const cleanContent = content.replace(/<think[^>]*>[\s\S]*?<\/think>/gi, '')
const hasToolCalls = toolCalls && toolCalls.length > 0
const renderedContent = useMemo(() => {
if (!cleanContent) return []
return renderContent(cleanContent)
}, [cleanContent])
const formattedThinking = useMemo(() => {
if (!thinking) return ''
return formatText(thinking)
}, [thinking])
return (
<div className="feed-item assistant">
<div className="feed-avatar ai-rank">
@@ -246,7 +258,7 @@ function StreamingItem({ content, thinking, toolCalls }) {
</span>
<span className="feed-role">{rank.label}</span>
</div>
{thinking && <ThinkingBlock content={thinking} done={false} />}
{thinking && <ThinkingBlock content={formattedThinking} raw done={false} />}
{hasToolCalls && toolCalls.map((tc, i) => (
<ToolCallBlock key={tc.call?.tool_call_id || i} call={tc.call} result={tc.result} />
))}
@@ -257,7 +269,7 @@ function StreamingItem({ content, thinking, toolCalls }) {
)}
{cleanContent && (
<div className="feed-content">
{renderContent(cleanContent).map((part, i) =>
{renderedContent.map((part, i) =>
part.type === 'code' ? (
<div key={i} className="studio-code-block">
{part.lang && <div className="studio-code-lang">{part.lang}</div>}