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
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:
@@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user