fix(ui): remove Tests tab, remove raw-md/collapse toggles, add Copy MD button, bump v0.9.1
All checks were successful
Stable Release / stable (push) Successful in 1m46s

- Remove Tests tab from navigation (browsertest still works via snippet/extension)
- Remove showRawMarkdown and collapseHistory toggles from Studio input bar
- Add "Copy MD" button on each assistant message header to copy raw markdown
- Bump version to 0.9.1

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
This commit is contained in:
Augustin
2026-04-28 11:48:37 +02:00
parent 5875dab17f
commit 3445726b67
3 changed files with 22 additions and 57 deletions

View File

@@ -7,7 +7,7 @@ import (
const ( const (
Name = "muyue" Name = "muyue"
Version = "0.9.0" Version = "0.9.1"
Author = "La Légion de Muyue" Author = "La Légion de Muyue"
) )

View File

@@ -1,5 +1,5 @@
import { useState, useEffect, useCallback, useMemo, useRef } from 'react' import { useState, useEffect, useCallback, useMemo, useRef } from 'react'
import { LayoutDashboard, Sparkles, Terminal, Settings, TestTube2 } from 'lucide-react' import { LayoutDashboard, Sparkles, Terminal, Settings } from 'lucide-react'
import api from '../api/client' import api from '../api/client'
import { getTheme, applyTheme } from '../themes' import { getTheme, applyTheme } from '../themes'
import { useI18n } from '../i18n' import { useI18n } from '../i18n'
@@ -7,7 +7,6 @@ import Dashboard from './Dashboard'
import Studio from './Studio' import Studio from './Studio'
import Shell from './Shell' import Shell from './Shell'
import Config from './Config' import Config from './Config'
import Tests from './Tests'
import OnboardingWizard from './OnboardingWizard' import OnboardingWizard from './OnboardingWizard'
export default function App() { export default function App() {
@@ -25,7 +24,6 @@ export default function App() {
{ id: 'dash', label: t('tabs.dashboard'), icon: <LayoutDashboard size={15} /> }, { id: 'dash', label: t('tabs.dashboard'), icon: <LayoutDashboard size={15} /> },
{ id: 'studio', label: t('tabs.studio'), icon: <Sparkles size={15} /> }, { id: 'studio', label: t('tabs.studio'), icon: <Sparkles size={15} /> },
{ id: 'shell', label: t('tabs.shell'), icon: <Terminal size={15} /> }, { id: 'shell', label: t('tabs.shell'), icon: <Terminal size={15} /> },
{ id: 'tests', label: 'Tests', icon: <TestTube2 size={15} /> },
{ id: 'config', label: t('tabs.config'), icon: <Settings size={15} /> }, { id: 'config', label: t('tabs.config'), icon: <Settings size={15} /> },
], [t]) ], [t])
@@ -56,8 +54,7 @@ export default function App() {
Digit1: 'dash', Digit1: 'dash',
Digit2: 'studio', Digit2: 'studio',
Digit3: 'shell', Digit3: 'shell',
Digit4: 'tests', Digit4: 'config',
Digit5: 'config',
} }
if (map[e.code]) { if (map[e.code]) {
e.preventDefault() e.preventDefault()
@@ -134,7 +131,6 @@ export default function App() {
<div className={activeTab === 'dash' ? '' : 'tab-hidden'}><Dashboard api={api} refreshRef={dashRefreshRef} /></div> <div className={activeTab === 'dash' ? '' : 'tab-hidden'}><Dashboard api={api} refreshRef={dashRefreshRef} /></div>
<div className={activeTab === 'studio' ? '' : 'tab-hidden'}><Studio api={api} /></div> <div className={activeTab === 'studio' ? '' : 'tab-hidden'}><Studio api={api} /></div>
<div className={activeTab === 'shell' ? '' : 'tab-hidden'}><Shell api={api} isSudo={isSudo} /></div> <div className={activeTab === 'shell' ? '' : 'tab-hidden'}><Shell api={api} isSudo={isSudo} /></div>
<div className={activeTab === 'tests' ? '' : 'tab-hidden'}><Tests api={api} /></div>
<div className={activeTab === 'config' ? '' : 'tab-hidden'}><Config api={api} /></div> <div className={activeTab === 'config' ? '' : 'tab-hidden'}><Config api={api} /></div>
</main> </main>

View File

@@ -277,22 +277,15 @@ function CodeBlockWithCopy({ part, index, copiedIdx, setCopiedIdx }) {
) )
} }
function FeedItem({ msg, activeAgents, onModeChange, collapseHistory }) { function FeedItem({ msg, activeAgents, onModeChange }) {
const isUser = msg.role === 'user' const isUser = msg.role === 'user'
const isSystem = msg.role === 'system' const isSystem = msg.role === 'system'
const rank = getRank(msg.role) const rank = getRank(msg.role)
const [copiedIdx, setCopiedIdx] = useState(null) const [copiedIdx, setCopiedIdx] = useState(null)
const [forceExpand, setForceExpand] = useState(false)
const timeStr = msg.time ? new Date(msg.time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '' const timeStr = msg.time ? new Date(msg.time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : ''
const [showRawMarkdown, setShowRawMarkdown] = useState(() => {
try { return localStorage.getItem('muyue.showRawMarkdown') === 'true' } catch { return false }
})
const renderMarkdown = useCallback((content) => {
return <MarkdownContent content={content} raw={showRawMarkdown} />
}, [showRawMarkdown])
let parsedToolCalls = null let parsedToolCalls = null
let parsedToolResults = null let parsedToolResults = null
@@ -336,6 +329,19 @@ function FeedItem({ msg, activeAgents, onModeChange, collapseHistory }) {
</span> </span>
<span className="feed-role">{rank.label}</span> <span className="feed-role">{rank.label}</span>
{timeStr && <span className="feed-time">{timeStr}</span>} {timeStr && <span className="feed-time">{timeStr}</span>}
{!isUser && !isSystem && (
<button
className="studio-copy-btn"
onClick={() => {
navigator.clipboard.writeText(displayContent)
setCopiedMsg(true)
setTimeout(() => setCopiedMsg(false), 1500)
}}
style={{ marginLeft: 'auto', fontSize: '0.7em', opacity: copiedMsg ? 1 : 0.5, transition: 'opacity 0.15s' }}
>
{copiedMsg ? '✓' : 'Copy MD'}
</button>
)}
</div> </div>
{msg.thinking && <ThinkingBlock content={msg.thinking} done raw />} {msg.thinking && <ThinkingBlock content={msg.thinking} done raw />}
{msg.images && msg.images.length > 0 && ( {msg.images && msg.images.length > 0 && (
@@ -374,7 +380,6 @@ function FeedItem({ msg, activeAgents, onModeChange, collapseHistory }) {
) )
} }
if (seg.type === 'tool') { if (seg.type === 'tool') {
if (compress && seg !== lastTool) return null
const r = seg.result const r = seg.result
const result = r && (r.content !== undefined || r.is_error !== undefined) const result = r && (r.content !== undefined || r.is_error !== undefined)
? { content: r.content, is_error: r.is_error } ? { content: r.content, is_error: r.is_error }
@@ -427,12 +432,11 @@ function FeedItem({ msg, activeAgents, onModeChange, collapseHistory }) {
) )
} }
function StreamingItem({ content, thinking, toolCalls, segments, activeAgents, onModeChange, collapseHistory }) { function StreamingItem({ content, thinking, toolCalls, segments, activeAgents, onModeChange }) {
const rank = RANKS.general const rank = RANKS.general
const cleanContent = content.replace(/<think[^>]*>[\s\S]*?<\/think>/gi, '') const cleanContent = content.replace(/<think[^>]*>[\s\S]*?<\/think>/gi, '')
const hasToolCalls = toolCalls && toolCalls.length > 0 const hasToolCalls = toolCalls && toolCalls.length > 0
const [copiedIdx, setCopiedIdx] = useState(null) const [copiedIdx, setCopiedIdx] = useState(null)
const [forceExpand, setForceExpand] = useState(false)
const renderedContent = useMemo(() => { const renderedContent = useMemo(() => {
if (!cleanContent) return null if (!cleanContent) return null
@@ -445,8 +449,6 @@ function StreamingItem({ content, thinking, toolCalls, segments, activeAgents, o
}, [thinking]) }, [thinking])
const hasOrderedSegments = segments && segments.some(s => s.type === 'tool') const hasOrderedSegments = segments && segments.some(s => s.type === 'tool')
const toolSegments = (segments || []).filter(s => s.type === 'tool')
const compress = collapseHistory && !forceExpand && toolSegments.length > 1
return ( return (
<div className="feed-item assistant"> <div className="feed-item assistant">
@@ -541,9 +543,6 @@ export default function Studio({ api }) {
const [advancedReflection, setAdvancedReflection] = useState(() => { const [advancedReflection, setAdvancedReflection] = useState(() => {
try { return localStorage.getItem('muyue.advancedReflection') === 'true' } catch { return false } try { return localStorage.getItem('muyue.advancedReflection') === 'true' } catch { return false }
}) })
const [collapseHistory, setCollapseHistory] = useState(() => {
try { return localStorage.getItem('muyue.collapseHistory') !== 'false' } catch { return true }
})
const MAX_CRUSH_AGENTS = 2 const MAX_CRUSH_AGENTS = 2
const MAX_CLAUDE_AGENTS = 2 const MAX_CLAUDE_AGENTS = 2
const messagesEnd = useRef(null) const messagesEnd = useRef(null)
@@ -959,7 +958,7 @@ export default function Studio({ api }) {
<span className="feed-summary-toggle">{summarizedExpanded ? 'masquer' : 'voir'}</span> <span className="feed-summary-toggle">{summarizedExpanded ? 'masquer' : 'voir'}</span>
</div> </div>
{summarizedExpanded && summarizedMsgs.map(msg => ( {summarizedExpanded && summarizedMsgs.map(msg => (
<FeedItem key={msg.id} msg={msg} activeAgents={activeAgents} onModeChange={handleToolModeChange} collapseHistory={collapseHistory} /> <FeedItem key={msg.id} msg={msg} activeAgents={activeAgents} onModeChange={handleToolModeChange} />
))} ))}
</div> </div>
) )
@@ -971,7 +970,7 @@ export default function Studio({ api }) {
<> <>
{renderSummaryBlock()} {renderSummaryBlock()}
{activeMsgs.slice(0, visibleCount).map(msg => ( {activeMsgs.slice(0, visibleCount).map(msg => (
<FeedItem key={msg.id} msg={msg} activeAgents={activeAgents} onModeChange={handleToolModeChange} collapseHistory={collapseHistory} /> <FeedItem key={msg.id} msg={msg} activeAgents={activeAgents} onModeChange={handleToolModeChange} />
))} ))}
<div className="feed-collapsed-messages" onClick={handleToggleCollapsed}> <div className="feed-collapsed-messages" onClick={handleToggleCollapsed}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
@@ -988,7 +987,7 @@ export default function Studio({ api }) {
<> <>
{renderSummaryBlock()} {renderSummaryBlock()}
{activeMsgs.map(msg => ( {activeMsgs.map(msg => (
<FeedItem key={msg.id} msg={msg} activeAgents={activeAgents} onModeChange={handleToolModeChange} collapseHistory={collapseHistory} /> <FeedItem key={msg.id} msg={msg} activeAgents={activeAgents} onModeChange={handleToolModeChange} />
))} ))}
</> </>
) )
@@ -1012,7 +1011,7 @@ export default function Studio({ api }) {
<div className="studio-feed" ref={feedRef}> <div className="studio-feed" ref={feedRef}>
{renderMessages()} {renderMessages()}
{(streaming || streamThinking || loading || streamToolCalls.length > 0) && ( {(streaming || streamThinking || loading || streamToolCalls.length > 0) && (
<StreamingItem content={streaming} thinking={streamThinking} toolCalls={streamToolCalls} segments={streamSegments} activeAgents={activeAgents} onModeChange={handleToolModeChange} collapseHistory={collapseHistory} /> <StreamingItem content={streaming} thinking={streamThinking} toolCalls={streamToolCalls} segments={streamSegments} activeAgents={activeAgents} onModeChange={handleToolModeChange} />
)} )}
<div ref={messagesEnd} style={{ height: '24px' }} /> <div ref={messagesEnd} style={{ height: '24px' }} />
</div> </div>
@@ -1108,36 +1107,6 @@ export default function Studio({ api }) {
<circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/> <circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/>
</svg> </svg>
</button> </button>
<button
className="studio-attach-btn"
onClick={() => {
const next = !showRawMarkdown
setShowRawMarkdown(next)
try { localStorage.setItem('muyue.showRawMarkdown', String(next)) } catch {}
}}
disabled={loading}
title={showRawMarkdown ? "Markdown brut: ON" : "Markdown rendu"}
style={showRawMarkdown ? { color: 'var(--accent, #6c5ce7)', borderColor: 'var(--accent, #6c5ce7)' } : undefined}
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>
</svg>
</button>
<button
className="studio-attach-btn"
onClick={() => {
const next = !collapseHistory
setCollapseHistory(next)
try { localStorage.setItem('muyue.collapseHistory', String(next)) } catch {}
}}
disabled={loading}
title={collapseHistory ? "Historique compressé (dernière action visible)" : "Historique complet (tout visible)"}
style={collapseHistory ? { color: 'var(--accent, #6c5ce7)', borderColor: 'var(--accent, #6c5ce7)' } : undefined}
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/>
</svg>
</button>
<textarea <textarea
ref={textareaRef} ref={textareaRef}
value={input} value={input}