feat: update message handling to use 'finish' property for step completion logic
This commit is contained in:
@@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
- Optimized session activity status handling.
|
||||||
|
|
||||||
|
|
||||||
## [1.3.6] - 2025-12-27
|
## [1.3.6] - 2025-12-27
|
||||||
|
|
||||||
- Added the ability to manage (connect/disconnect) providers in settings.
|
- Added the ability to manage (connect/disconnect) providers in settings.
|
||||||
|
|||||||
Generated
+1
-1
@@ -2847,7 +2847,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openchamber-desktop"
|
name = "openchamber-desktop"
|
||||||
version = "1.3.5"
|
version = "1.3.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
|
|||||||
@@ -329,8 +329,8 @@ async fn handle_event(
|
|||||||
set_phase(app, &id, ActivityPhase::Busy, phases.clone(), cooldowns.clone()).await;
|
set_phase(app, &id, ActivityPhase::Busy, phases.clone(), cooldowns.clone()).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Derive cooldown from "step-finish reason=stop" marker when present.
|
// Derive cooldown from info.finish === 'stop' when present.
|
||||||
if is_stop_step_finish_part(&event.properties) {
|
if has_finish_stop(info) {
|
||||||
enter_cooldown_if_busy(app, &id, phases.clone(), cooldowns.clone()).await;
|
enter_cooldown_if_busy(app, &id, phases.clone(), cooldowns.clone()).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -349,13 +349,8 @@ fn is_streaming_assistant_part(properties: &Value) -> bool {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_stop_step_finish_part(properties: &Value) -> bool {
|
fn has_finish_stop(info: &Value) -> bool {
|
||||||
let Some(part) = properties.get("part") else {
|
info.get("finish").and_then(Value::as_str) == Some("stop")
|
||||||
return false;
|
|
||||||
};
|
|
||||||
let part_type = part.get("type").and_then(Value::as_str);
|
|
||||||
let reason = part.get("reason").and_then(Value::as_str);
|
|
||||||
part_type == Some("step-finish") && reason == Some("stop")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn enter_cooldown_if_busy(
|
async fn enter_cooldown_if_busy(
|
||||||
|
|||||||
@@ -287,6 +287,11 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
|
|||||||
return Boolean(messageCompletedAt && messageCompletedAt > 0);
|
return Boolean(messageCompletedAt && messageCompletedAt > 0);
|
||||||
}, [isUser, messageCompletedAt]);
|
}, [isUser, messageCompletedAt]);
|
||||||
|
|
||||||
|
const messageFinish = React.useMemo(() => {
|
||||||
|
const finish = (message.info as { finish?: string }).finish;
|
||||||
|
return typeof finish === 'string' ? finish : undefined;
|
||||||
|
}, [message.info]);
|
||||||
|
|
||||||
const visibleParts = React.useMemo(
|
const visibleParts = React.useMemo(
|
||||||
() =>
|
() =>
|
||||||
filterVisibleParts(normalizedParts, {
|
filterVisibleParts(normalizedParts, {
|
||||||
@@ -382,23 +387,8 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
|
|||||||
return { name, token: rawValue } satisfies AgentMentionInfo;
|
return { name, token: rawValue } satisfies AgentMentionInfo;
|
||||||
}, [isUser, message.parts]);
|
}, [isUser, message.parts]);
|
||||||
|
|
||||||
const stepState = React.useMemo(() => {
|
// Message is considered to have an "open step" if info.finish is not yet present
|
||||||
|
const hasOpenStep = typeof messageFinish !== 'string';
|
||||||
let stepStarts = 0;
|
|
||||||
let stepFinishes = 0;
|
|
||||||
visibleParts.forEach((part) => {
|
|
||||||
if (part.type === 'step-start') {
|
|
||||||
stepStarts += 1;
|
|
||||||
} else if (part.type === 'step-finish') {
|
|
||||||
stepFinishes += 1;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
hasOpenStep: stepStarts > stepFinishes,
|
|
||||||
};
|
|
||||||
}, [visibleParts]);
|
|
||||||
|
|
||||||
const hasOpenStep = stepState.hasOpenStep;
|
|
||||||
|
|
||||||
const shouldCoordinateRendering = React.useMemo(() => {
|
const shouldCoordinateRendering = React.useMemo(() => {
|
||||||
if (isUser) {
|
if (isUser) {
|
||||||
@@ -756,6 +746,7 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
|
|||||||
parts={visibleParts}
|
parts={visibleParts}
|
||||||
isUser={isUser}
|
isUser={isUser}
|
||||||
isMessageCompleted={isMessageCompleted}
|
isMessageCompleted={isMessageCompleted}
|
||||||
|
messageFinish={messageFinish}
|
||||||
syntaxTheme={syntaxTheme}
|
syntaxTheme={syntaxTheme}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
hasTouchInput={hasTouchInput}
|
hasTouchInput={hasTouchInput}
|
||||||
@@ -797,6 +788,7 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
|
|||||||
parts={visibleParts}
|
parts={visibleParts}
|
||||||
isUser={isUser}
|
isUser={isUser}
|
||||||
isMessageCompleted={isMessageCompleted}
|
isMessageCompleted={isMessageCompleted}
|
||||||
|
messageFinish={messageFinish}
|
||||||
syntaxTheme={syntaxTheme}
|
syntaxTheme={syntaxTheme}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
hasTouchInput={hasTouchInput}
|
hasTouchInput={hasTouchInput}
|
||||||
|
|||||||
@@ -108,22 +108,15 @@ export const detectTurns = (messages: ChatMessageEntry[]): Turn[] => {
|
|||||||
const extractFinalAssistantText = (turn: Turn): string | undefined => {
|
const extractFinalAssistantText = (turn: Turn): string | undefined => {
|
||||||
|
|
||||||
for (const assistantMsg of turn.assistantMessages) {
|
for (const assistantMsg of turn.assistantMessages) {
|
||||||
|
const infoFinish = (assistantMsg.info as { finish?: string | null | undefined }).finish;
|
||||||
|
|
||||||
for (const part of assistantMsg.parts) {
|
if (infoFinish === 'stop') {
|
||||||
if (part.type === 'step-finish') {
|
const textPart = assistantMsg.parts.find(p => p.type === 'text');
|
||||||
const finishReason = (part as { reason?: string | null | undefined }).reason;
|
if (textPart) {
|
||||||
const infoFinish = (assistantMsg.info as { finish?: string | null | undefined }).finish;
|
const textContent = (textPart as { text?: string | null | undefined }).text ??
|
||||||
|
(textPart as { content?: string | null | undefined }).content;
|
||||||
if (finishReason === 'stop' || infoFinish === 'stop') {
|
if (typeof textContent === 'string' && textContent.trim().length > 0) {
|
||||||
|
return textContent;
|
||||||
const textPart = assistantMsg.parts.find(p => p.type === 'text');
|
|
||||||
if (textPart) {
|
|
||||||
const textContent = (textPart as { text?: string | null | undefined }).text ??
|
|
||||||
(textPart as { content?: string | null | undefined }).content;
|
|
||||||
if (typeof textContent === 'string' && textContent.trim().length > 0) {
|
|
||||||
return textContent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -194,12 +187,9 @@ const getTurnActivityInfo = (turn: Turn): TurnActivityInfo => {
|
|||||||
|
|
||||||
turn.assistantMessages.forEach((msg) => {
|
turn.assistantMessages.forEach((msg) => {
|
||||||
const messageId = msg.info.id;
|
const messageId = msg.info.id;
|
||||||
|
const infoFinish = (msg.info as { finish?: string | null | undefined }).finish;
|
||||||
const hasStopFinishInMessage = ENABLE_TEXT_JUSTIFICATION_ACTIVITY
|
const hasStopFinishInMessage = ENABLE_TEXT_JUSTIFICATION_ACTIVITY
|
||||||
? msg.parts.some((part) => {
|
? infoFinish === 'stop'
|
||||||
if (part.type !== 'step-finish') return false;
|
|
||||||
const reason = (part as { reason?: string | null | undefined }).reason;
|
|
||||||
return reason === 'stop';
|
|
||||||
})
|
|
||||||
: false;
|
: false;
|
||||||
|
|
||||||
msg.parts.forEach((part) => {
|
msg.parts.forEach((part) => {
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ interface MessageBodyProps {
|
|||||||
parts: Part[];
|
parts: Part[];
|
||||||
isUser: boolean;
|
isUser: boolean;
|
||||||
isMessageCompleted: boolean;
|
isMessageCompleted: boolean;
|
||||||
|
messageFinish?: string;
|
||||||
|
|
||||||
syntaxTheme: { [key: string]: React.CSSProperties };
|
syntaxTheme: { [key: string]: React.CSSProperties };
|
||||||
|
|
||||||
@@ -300,6 +301,7 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
|
|||||||
messageId,
|
messageId,
|
||||||
parts,
|
parts,
|
||||||
isMessageCompleted,
|
isMessageCompleted,
|
||||||
|
messageFinish,
|
||||||
|
|
||||||
syntaxTheme,
|
syntaxTheme,
|
||||||
isMobile,
|
isMobile,
|
||||||
@@ -347,9 +349,7 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
|
|||||||
|
|
||||||
const createSessionFromAssistantMessage = useSessionStore((state) => state.createSessionFromAssistantMessage);
|
const createSessionFromAssistantMessage = useSessionStore((state) => state.createSessionFromAssistantMessage);
|
||||||
const isLastAssistantInTurn = turnGroupingContext?.isLastAssistantInTurn ?? false;
|
const isLastAssistantInTurn = turnGroupingContext?.isLastAssistantInTurn ?? false;
|
||||||
const hasStopFinish = React.useMemo(() => {
|
const hasStopFinish = messageFinish === 'stop';
|
||||||
return parts.some((part) => part.type === 'step-finish' && (part as { reason?: string | null | undefined }).reason === 'stop');
|
|
||||||
}, [parts]);
|
|
||||||
|
|
||||||
|
|
||||||
const hasTools = toolParts.length > 0;
|
const hasTools = toolParts.length > 0;
|
||||||
@@ -414,24 +414,8 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
|
|||||||
});
|
});
|
||||||
}, [reasoningParts]);
|
}, [reasoningParts]);
|
||||||
|
|
||||||
const stepState = React.useMemo(() => {
|
// Message is considered to have an "open step" if info.finish is not yet present
|
||||||
let stepStarts = 0;
|
const hasOpenStep = typeof messageFinish !== 'string';
|
||||||
let stepFinishes = 0;
|
|
||||||
visibleParts.forEach((part) => {
|
|
||||||
if (part.type === 'step-start') {
|
|
||||||
stepStarts += 1;
|
|
||||||
} else if (part.type === 'step-finish') {
|
|
||||||
stepFinishes += 1;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
stepStarts,
|
|
||||||
stepFinishes,
|
|
||||||
hasOpenStep: stepStarts > stepFinishes,
|
|
||||||
};
|
|
||||||
}, [visibleParts]);
|
|
||||||
|
|
||||||
const hasOpenStep = stepState.hasOpenStep;
|
|
||||||
|
|
||||||
const shouldHoldForReasoning =
|
const shouldHoldForReasoning =
|
||||||
reasoningParts.length > 0 &&
|
reasoningParts.length > 0 &&
|
||||||
|
|||||||
@@ -698,9 +698,7 @@ export const useEventStream = () => {
|
|||||||
|
|
||||||
const existingMessage = getMessageFromStore(sessionId, messageId);
|
const existingMessage = getMessageFromStore(sessionId, messageId);
|
||||||
const existingLen = computeTextLength(existingMessage?.parts || []);
|
const existingLen = computeTextLength(existingMessage?.parts || []);
|
||||||
const existingStopMarker = existingMessage?.parts?.some(
|
const existingStopMarker = (existingMessage?.info as { finish?: string } | undefined)?.finish === 'stop';
|
||||||
(part) => part?.type === 'step-finish' && (part as { reason?: string }).reason === 'stop'
|
|
||||||
) ?? false;
|
|
||||||
|
|
||||||
const serverParts = (props as { parts?: unknown }).parts || (messageExt as { parts?: unknown }).parts;
|
const serverParts = (props as { parts?: unknown }).parts || (messageExt as { parts?: unknown }).parts;
|
||||||
const partsArray = Array.isArray(serverParts) ? (serverParts as Part[]) : [];
|
const partsArray = Array.isArray(serverParts) ? (serverParts as Part[]) : [];
|
||||||
@@ -710,12 +708,13 @@ export const useEventStream = () => {
|
|||||||
|
|
||||||
if (!hasParts && !completedFromServer) break;
|
if (!hasParts && !completedFromServer) break;
|
||||||
|
|
||||||
|
const finishCandidate = (message as { finish?: unknown }).finish;
|
||||||
|
const finish = typeof finishCandidate === 'string' ? finishCandidate : null;
|
||||||
|
const eventHasStopFinish = finish === 'stop';
|
||||||
|
|
||||||
if ((messageExt as { role?: unknown }).role === 'assistant' && hasParts) {
|
if ((messageExt as { role?: unknown }).role === 'assistant' && hasParts) {
|
||||||
const incomingLen = computeTextLength(partsArray);
|
const incomingLen = computeTextLength(partsArray);
|
||||||
const wouldShrink = existingLen > 0 && incomingLen + TEXT_SHRINK_TOLERANCE < existingLen;
|
const wouldShrink = existingLen > 0 && incomingLen + TEXT_SHRINK_TOLERANCE < existingLen;
|
||||||
const eventHasStopFinish = partsArray.some(
|
|
||||||
(p) => p?.type === 'step-finish' && (p as { reason?: string }).reason === 'stop'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (wouldShrink && !eventHasStopFinish) {
|
if (wouldShrink && !eventHasStopFinish) {
|
||||||
trackMessage(messageId, 'skipped_shrinking_update', { incomingLen, existingLen });
|
trackMessage(messageId, 'skipped_shrinking_update', { incomingLen, existingLen });
|
||||||
@@ -772,16 +771,12 @@ export const useEventStream = () => {
|
|||||||
const messageTime = (message as { time?: { completed?: unknown } }).time;
|
const messageTime = (message as { time?: { completed?: unknown } }).time;
|
||||||
const completedCandidate = (messageTime as { completed?: unknown } | undefined)?.completed;
|
const completedCandidate = (messageTime as { completed?: unknown } | undefined)?.completed;
|
||||||
const hasCompletedTimestamp = typeof completedCandidate === 'number' && Number.isFinite(completedCandidate);
|
const hasCompletedTimestamp = typeof completedCandidate === 'number' && Number.isFinite(completedCandidate);
|
||||||
const finishCandidate = (message as { finish?: unknown }).finish;
|
|
||||||
const finish = typeof finishCandidate === 'string' ? finishCandidate : null;
|
|
||||||
|
|
||||||
const stopMarkerPresent = partsArray.some(
|
const stopMarkerPresent = finish === 'stop' || existingStopMarker;
|
||||||
(p) => p?.type === 'step-finish' && (p as { reason?: string }).reason === 'stop'
|
|
||||||
) || existingStopMarker;
|
|
||||||
|
|
||||||
const shouldFinalizeAssistantMessage =
|
const shouldFinalizeAssistantMessage =
|
||||||
(message as { role?: string }).role === 'assistant' &&
|
(message as { role?: string }).role === 'assistant' &&
|
||||||
(hasCompletedTimestamp || finish === 'stop' || stopMarkerPresent);
|
(hasCompletedTimestamp || stopMarkerPresent);
|
||||||
|
|
||||||
if (shouldFinalizeAssistantMessage && (message as { role?: string }).role === 'assistant') {
|
if (shouldFinalizeAssistantMessage && (message as { role?: string }).role === 'assistant') {
|
||||||
|
|
||||||
@@ -894,20 +889,9 @@ export const useEventStream = () => {
|
|||||||
completeStreamingMessage(sessionId, messageId);
|
completeStreamingMessage(sessionId, messageId);
|
||||||
|
|
||||||
// For web/vscode: trigger cooldown only when assistant message has finish === "stop"
|
// For web/vscode: trigger cooldown only when assistant message has finish === "stop"
|
||||||
// (or we can infer a stop marker) to match desktop backend semantics.
|
// to match desktop backend semantics.
|
||||||
if (!isDesktopRuntimeRef.current) {
|
if (!isDesktopRuntimeRef.current) {
|
||||||
const finishCandidate = (message as { finish?: unknown }).finish;
|
if (finish === 'stop') {
|
||||||
const finish = typeof finishCandidate === 'string' ? finishCandidate : null;
|
|
||||||
|
|
||||||
const inferredStopMarkerPresent =
|
|
||||||
Array.isArray(partsArray) &&
|
|
||||||
partsArray.some((part) => {
|
|
||||||
if (!part || typeof part !== 'object') return false;
|
|
||||||
const partAny = part as { type?: string; reason?: string };
|
|
||||||
return partAny.type === 'step-finish' && partAny.reason === 'stop';
|
|
||||||
});
|
|
||||||
|
|
||||||
if (finish === 'stop' || inferredStopMarkerPresent) {
|
|
||||||
const currentPhase = useSessionStore.getState().sessionActivityPhase?.get(sessionId);
|
const currentPhase = useSessionStore.getState().sessionActivityPhase?.get(sessionId);
|
||||||
if (currentPhase === 'busy') {
|
if (currentPhase === 'busy') {
|
||||||
updateSessionActivityPhase(sessionId, 'cooldown');
|
updateSessionActivityPhase(sessionId, 'cooldown');
|
||||||
|
|||||||
@@ -590,7 +590,7 @@ export const debugUtils = {
|
|||||||
|
|
||||||
const lastMessage = assistantMessages[assistantMessages.length - 1];
|
const lastMessage = assistantMessages[assistantMessages.length - 1];
|
||||||
const stepFinishParts = lastMessage.parts.filter((p: any) => p.type === 'step-finish');
|
const stepFinishParts = lastMessage.parts.filter((p: any) => p.type === 'step-finish');
|
||||||
const hasStopReason = lastMessage.parts.some((p: any) => p.type === 'step-finish' && p.reason === 'stop');
|
const hasStopReason = (lastMessage.info as { finish?: string }).finish === 'stop';
|
||||||
|
|
||||||
const timeInfo = lastMessage.info.time as any;
|
const timeInfo = lastMessage.info.time as any;
|
||||||
const completedAt = timeInfo?.completed;
|
const completedAt = timeInfo?.completed;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export interface MessageInfo {
|
|||||||
};
|
};
|
||||||
status?: string;
|
status?: string;
|
||||||
streaming?: boolean;
|
streaming?: boolean;
|
||||||
|
finish?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MessageRecord {
|
export interface MessageRecord {
|
||||||
@@ -27,9 +28,7 @@ export function isMessageComplete(messageInfo: MessageInfo, parts: Part[] = []):
|
|||||||
const completedAt = typeof timeInfo?.completed === 'number' ? timeInfo.completed : undefined;
|
const completedAt = typeof timeInfo?.completed === 'number' ? timeInfo.completed : undefined;
|
||||||
const messageStatus = messageInfo?.status;
|
const messageStatus = messageInfo?.status;
|
||||||
|
|
||||||
const hasStopFinish = parts.some(part =>
|
const hasStopFinish = messageInfo.finish === 'stop';
|
||||||
part.type === 'step-finish' && (part as any).reason === 'stop'
|
|
||||||
);
|
|
||||||
|
|
||||||
const hasCompletedFlag = (typeof completedAt === 'number' && completedAt > 0) || messageStatus === 'completed';
|
const hasCompletedFlag = (typeof completedAt === 'number' && completedAt > 0) || messageStatus === 'completed';
|
||||||
if (!hasCompletedFlag || !hasStopFinish) {
|
if (!hasCompletedFlag || !hasStopFinish) {
|
||||||
|
|||||||
@@ -101,13 +101,8 @@ const computePartsTextLength = (parts: Part[] | undefined): number => {
|
|||||||
}, 0);
|
}, 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasStopReasonStop = (parts: Part[] | undefined): boolean => {
|
const hasFinishStop = (info: { finish?: string } | undefined): boolean => {
|
||||||
if (!Array.isArray(parts)) {
|
return info?.finish === "stop";
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return parts.some(
|
|
||||||
(part) => part?.type === "step-finish" && (part as Record<string, unknown>)?.reason === "stop"
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPartKey = (part: Part | undefined): string | undefined => {
|
const getPartKey = (part: Part | undefined): string | undefined => {
|
||||||
@@ -159,8 +154,8 @@ const mergeDuplicateMessage = (
|
|||||||
const incomingParts = Array.isArray(incoming.parts) ? incoming.parts : [];
|
const incomingParts = Array.isArray(incoming.parts) ? incoming.parts : [];
|
||||||
const existingLen = computePartsTextLength(existingParts);
|
const existingLen = computePartsTextLength(existingParts);
|
||||||
const incomingLen = computePartsTextLength(incomingParts);
|
const incomingLen = computePartsTextLength(incomingParts);
|
||||||
const existingStop = hasStopReasonStop(existingParts);
|
const existingStop = hasFinishStop(existing.info);
|
||||||
const incomingStop = hasStopReasonStop(incomingParts);
|
const incomingStop = hasFinishStop(incoming.info);
|
||||||
|
|
||||||
let parts = incomingParts;
|
let parts = incomingParts;
|
||||||
if (existingStop && existingLen >= incomingLen) {
|
if (existingStop && existingLen >= incomingLen) {
|
||||||
@@ -449,7 +444,7 @@ export const useMessageStore = create<MessageStore>()(
|
|||||||
const existingParts = Array.isArray(existingEntry.parts) ? existingEntry.parts : [];
|
const existingParts = Array.isArray(existingEntry.parts) ? existingEntry.parts : [];
|
||||||
const existingLen = computePartsTextLength(existingParts);
|
const existingLen = computePartsTextLength(existingParts);
|
||||||
const serverLen = computePartsTextLength(serverParts);
|
const serverLen = computePartsTextLength(serverParts);
|
||||||
const storeHasStop = hasStopReasonStop(existingParts);
|
const storeHasStop = hasFinishStop(existingEntry.info);
|
||||||
|
|
||||||
if (storeHasStop && existingLen > serverLen) {
|
if (storeHasStop && existingLen > serverLen) {
|
||||||
const mergedParts = mergePreferExistingParts(existingParts, serverParts);
|
const mergedParts = mergePreferExistingParts(existingParts, serverParts);
|
||||||
@@ -1388,14 +1383,6 @@ export const useMessageStore = create<MessageStore>()(
|
|||||||
return finalizeAbortState({ messages: newMessages, ...updates });
|
return finalizeAbortState({ messages: newMessages, ...updates });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const partType = (part as any)?.type;
|
|
||||||
if (partType === 'step-finish' && actualRole !== 'user') {
|
|
||||||
setTimeout(() => {
|
|
||||||
const store = get();
|
|
||||||
store.completeStreamingMessage(sessionId, messageId);
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
addStreamingPart: (sessionId: string, messageId: string, part: Part, role?: string, currentSessionId?: string) => {
|
addStreamingPart: (sessionId: string, messageId: string, part: Part, role?: string, currentSessionId?: string) => {
|
||||||
@@ -1887,6 +1874,16 @@ export const useMessageStore = create<MessageStore>()(
|
|||||||
|
|
||||||
return updates;
|
return updates;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Trigger completion when info.finish is present for assistant messages
|
||||||
|
const infoFinish = (messageInfo as { finish?: string })?.finish;
|
||||||
|
const messageRole = (messageInfo as { role?: string })?.role;
|
||||||
|
if (typeof infoFinish === 'string' && messageRole !== 'user') {
|
||||||
|
setTimeout(() => {
|
||||||
|
const store = get();
|
||||||
|
store.completeStreamingMessage(sessionId, messageId);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
completeStreamingMessage: (sessionId: string, messageId: string) => {
|
completeStreamingMessage: (sessionId: string, messageId: string) => {
|
||||||
@@ -2041,7 +2038,7 @@ export const useMessageStore = create<MessageStore>()(
|
|||||||
const existingParts = Array.isArray(existingEntry.parts) ? existingEntry.parts : [];
|
const existingParts = Array.isArray(existingEntry.parts) ? existingEntry.parts : [];
|
||||||
const existingLen = computePartsTextLength(existingParts);
|
const existingLen = computePartsTextLength(existingParts);
|
||||||
const serverLen = computePartsTextLength(serverParts);
|
const serverLen = computePartsTextLength(serverParts);
|
||||||
const storeHasStop = hasStopReasonStop(existingParts);
|
const storeHasStop = hasFinishStop(existingEntry.info);
|
||||||
|
|
||||||
if (storeHasStop && existingLen > serverLen) {
|
if (storeHasStop && existingLen > serverLen) {
|
||||||
const mergedParts = mergePreferExistingParts(existingParts, serverParts);
|
const mergedParts = mergePreferExistingParts(existingParts, serverParts);
|
||||||
|
|||||||
@@ -864,24 +864,6 @@ function deriveSessionActivity(payload) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payload.type === 'message.part.updated') {
|
|
||||||
const info = payload.properties?.info;
|
|
||||||
const part = payload.properties?.part;
|
|
||||||
const sessionId = info?.sessionID ?? part?.sessionID ?? payload.properties?.sessionID;
|
|
||||||
const role = info?.role;
|
|
||||||
const partType = part?.type;
|
|
||||||
const reason = part?.reason;
|
|
||||||
if (
|
|
||||||
typeof sessionId === 'string' &&
|
|
||||||
sessionId.length > 0 &&
|
|
||||||
role === 'assistant' &&
|
|
||||||
partType === 'step-finish' &&
|
|
||||||
reason === 'stop'
|
|
||||||
) {
|
|
||||||
return { sessionId, phase: 'cooldown' };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payload.type === 'session.idle') {
|
if (payload.type === 'session.idle') {
|
||||||
const sessionId = payload.properties?.sessionID;
|
const sessionId = payload.properties?.sessionID;
|
||||||
if (typeof sessionId === 'string' && sessionId.length > 0) {
|
if (typeof sessionId === 'string' && sessionId.length > 0) {
|
||||||
|
|||||||
Reference in New Issue
Block a user