feat: improve chat streaming UX and add Mermaid diagram rendering (#438)

* feat: show current branch in empty chat state

* fix: display current git branch for worktrees and update them with branch change

* refactor: improve read tool output parsing with structured data

* feat: add support for message part delta events

* fix(chat): improve streaming rendering, scroll behavior, and assistant action visibility

* feat: Add mermaid diagram support to chat markdown rendering

* fix: update table download functionality to include success notification and remove unused MarkdownRenderer import

* refactor: streamline Streamdown component props for improved readability

* fix: preserve Streamdown code-block markers and use native Tauri cache clearing

* feat: add context overview panel to view conversation details
This commit is contained in:
Bohdan Triapitsyn
2026-02-17 18:25:03 +02:00
committed by GitHub
parent 138772e66e
commit 4d71bb27eb
31 changed files with 1391 additions and 182 deletions
+75 -1
View File
@@ -818,7 +818,6 @@ export const useEventStream = () => {
type: part.type || 'text',
} as Part;
// Fallback: if we see assistant parts but session.status hasn't arrived yet, mark busy.
if (roleInfo === 'assistant') {
const partType = (messagePart as { type?: unknown }).type;
const partTime = (messagePart as { time?: { end?: unknown } }).time;
@@ -863,6 +862,81 @@ export const useEventStream = () => {
break;
}
case 'message.part.delta': {
const sessionId = readStringProp(props, ['sessionID', 'sessionId']);
const messageId = readStringProp(props, ['messageID', 'messageId']);
const partId = readStringProp(props, ['partID', 'partId']);
const field = readStringProp(props, ['field']);
const delta = typeof props.delta === 'string' ? props.delta : null;
if (!sessionId || !messageId || !partId || !field || delta === null) {
if (streamDebugEnabled()) {
console.debug('[useEventStream] Skipping message.part.delta with missing payload', {
sessionID: props.sessionID,
messageID: props.messageID,
partID: props.partID,
field: props.field,
});
}
break;
}
lastMessageEventBySessionRef.current.set(sessionId, Date.now());
const pendingTimer = pendingMessageStallTimersRef.current.get(sessionId);
if (pendingTimer) {
clearTimeout(pendingTimer);
pendingMessageStallTimersRef.current.delete(sessionId);
}
const trimmedHeadMaxId = useSessionStore.getState().sessionMemoryState.get(sessionId)?.trimmedHeadMaxId;
if (trimmedHeadMaxId && !isIdNewer(messageId, trimmedHeadMaxId)) {
if (streamDebugEnabled()) {
console.debug('[useEventStream] Skipping message.part.delta for trimmed message', {
sessionId,
messageId,
trimmedHeadMaxId,
});
}
break;
}
const existingMessage = getMessageFromStore(sessionId, messageId);
const existingPart = existingMessage?.parts?.find((item) => item?.id === partId);
if (!existingPart) {
break;
}
const existingPartRecord = existingPart as Record<string, unknown>;
const existingFieldValue = existingPartRecord[field];
const updatedPart: Part = {
...existingPart,
[field]: `${typeof existingFieldValue === 'string' ? existingFieldValue : ''}${delta}`,
} as Part;
let roleInfo = 'assistant';
const existingRole = (existingMessage?.info as Record<string, unknown> | undefined)?.role;
if (typeof existingRole === 'string') {
roleInfo = existingRole;
}
if (roleInfo === 'assistant' && delta.length > 0) {
const currentStatus = useSessionStore.getState().sessionStatus?.get(sessionId);
const recentlyConfirmedIdle =
currentStatus?.type === 'idle' &&
typeof currentStatus.confirmedAt === 'number' &&
Date.now() - currentStatus.confirmedAt < 1200;
if (!currentStatus || currentStatus.type === 'idle') {
if (!recentlyConfirmedIdle) {
updateSessionStatus(sessionId, { type: 'busy' }, 'sse:message.part.delta');
}
}
}
trackMessage(messageId, 'part_delta_received', { role: roleInfo, field });
addStreamingPart(sessionId, messageId, updatedPart, roleInfo);
break;
}
case 'message.updated': {
const message = (typeof props.info === 'object' && props.info !== null) ? (props.info as Record<string, unknown>) : props;
const messageExt = message as Record<string, unknown>;