fix(studio): improve chat context, thinking tags, streaming, and tool results
All checks were successful
Beta Release / beta (push) Successful in 39s

- Fix cleanThinkingTags to use proper regex instead of naive ReplaceAll
- Send conversation history (last 20 messages + summary) to AI instead of single message
- Store tool results alongside tool calls so history shows complete execution info
- Stream words instead of characters for smoother SSE rendering
- Add stop button to cancel in-progress AI requests (AbortController)
- Fix markdown rendering: add h2 support, use div for bullets
- Add i18n keys for cancel/stop (EN + FR)

💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
This commit is contained in:
Augustin
2026-04-22 22:37:45 +02:00
parent 2e50366cd8
commit 65804aae4e
7 changed files with 149 additions and 26 deletions

View File

@@ -59,8 +59,9 @@ function formatText(text) {
.replace(/`([^`]+)`/g, '<code class="inline-code">$1</code>')
.replace(/^### (.+)$/gm, '<h4 class="msg-h4">$1</h4>')
.replace(/^## (.+)$/gm, '<h3 class="msg-h3">$1</h3>')
.replace(/^\s*[-*] (.+)$/gm, '<span class="msg-bullet">$1</span>')
.replace(/^\s*(\d+)[.)] (.+)$/gm, '<span class="msg-step"><span class="msg-step-num">$1</span>$2</span>')
.replace(/^# (.+)$/gm, '<h2 class="msg-h2">$1</h2>')
.replace(/^\s*[-*] (.+)$/gm, '<div class="msg-bullet">• $1</div>')
.replace(/^\s*(\d+)[.)] (.+)$/gm, '<div class="msg-step"><span class="msg-step-num">$1</span> $2</div>')
}
function ThinkingBlock({ content, done }) {
@@ -151,11 +152,13 @@ function FeedItem({ msg }) {
const timeStr = msg.time ? new Date(msg.time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : ''
let parsedToolCalls = null
let parsedToolResults = null
let displayContent = msg.content
try {
const parsed = JSON.parse(msg.content)
if (parsed && Array.isArray(parsed.tool_calls)) {
parsedToolCalls = parsed.tool_calls
parsedToolResults = parsed.tool_results || null
displayContent = parsed.content || ''
}
} catch {}
@@ -186,9 +189,15 @@ function FeedItem({ msg }) {
{timeStr && <span className="feed-time">{timeStr}</span>}
</div>
{msg.thinking && <ThinkingBlock content={msg.thinking} done />}
{parsedToolCalls && parsedToolCalls.map((tc, i) => (
<ToolCallBlock key={tc.tool_call_id || i} call={tc} result={null} />
))}
{parsedToolCalls && parsedToolCalls.map((tc, i) => {
const resultData = parsedToolResults
? parsedToolResults.find(r => r.tool_call_id === tc.tool_call_id)
: null
const result = resultData
? { content: resultData.result, is_error: resultData.is_error }
: null
return <ToolCallBlock key={tc.tool_call_id || i} call={tc} result={result} />
})}
{cleanContent && (
<div className="feed-content">
{renderContent(cleanContent).map((part, i) =>
@@ -265,6 +274,7 @@ export default function Studio({ api }) {
const [loaded, setLoaded] = useState(false)
const messagesEnd = useRef(null)
const textareaRef = useRef(null)
const abortRef = useRef(null)
useEffect(() => {
api.getChatHistory().then(data => {
@@ -321,6 +331,9 @@ export default function Studio({ api }) {
setStreamThinking('')
setStreamToolCalls([])
const controller = new AbortController()
abortRef.current = controller
try {
let accumulated = ''
let thinking = ''
@@ -349,7 +362,7 @@ export default function Studio({ api }) {
}
accumulated = partial
setStreaming(partial)
})
}, controller.signal)
const finalContent = accumulated || t('studio.noResponse')
const aiMsg = {
@@ -367,19 +380,37 @@ export default function Studio({ api }) {
}
setMessages(prev => [...prev, aiMsg])
} catch (err) {
setMessages(prev => [...prev, {
id: (Date.now() + 1).toString(),
role: 'system',
content: `${t('studio.error')}: ${err.message}`,
time: new Date().toISOString(),
}])
if (err.name === 'AbortError') {
if (streaming) {
setMessages(prev => [...prev, {
id: (Date.now() + 1).toString(),
role: 'system',
content: t('studio.cancelled'),
time: new Date().toISOString(),
}])
}
} else {
setMessages(prev => [...prev, {
id: (Date.now() + 1).toString(),
role: 'system',
content: `${t('studio.error')}: ${err.message}`,
time: new Date().toISOString(),
}])
}
} finally {
setLoading(false)
setStreaming('')
setStreamThinking('')
setStreamToolCalls([])
abortRef.current = null
}
}, [input, loading, api, t, handleClear])
}, [input, loading, api, t, handleClear, streaming])
const handleStop = useCallback(() => {
if (abortRef.current) {
abortRef.current.abort()
}
}, [])
const handleKeyDown = (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
@@ -432,6 +463,13 @@ export default function Studio({ api }) {
<line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>
</svg>
</button>
{loading && (
<button className="studio-stop-btn" onClick={handleStop} title={t('studio.stop')}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<rect x="4" y="4" width="16" height="16" rx="2"/>
</svg>
</button>
)}
</div>
<div className="studio-input-hint">
{t('studio.inputHint')} &middot; /clear