diff --git a/web/hwlab-cloud-web/src/stores/workbench-live-kafka-event.ts b/web/hwlab-cloud-web/src/stores/workbench-live-kafka-event.ts index ad101f47..5d8e1e04 100644 --- a/web/hwlab-cloud-web/src/stores/workbench-live-kafka-event.ts +++ b/web/hwlab-cloud-web/src/stores/workbench-live-kafka-event.ts @@ -43,8 +43,7 @@ export function reduceWorkbenchLiveKafkaMessageState(previous: WorkbenchLiveMess if (previous.terminal) return previous; const terminal = workbenchLiveKafkaEventIsTerminal(event); const status = terminal ? firstNonEmptyString(event.status, "completed") ?? "completed" : "running"; - const lifecycleEvents = [...previousEvents, event]; - const assistantText = traceAssistantLifecycleEventIsSuperseded(event, lifecycleEvents, lifecycleEvents.length - 1) + const assistantText = traceAssistantLifecycleEventIsSuperseded(event, previousEvents, previousEvents.length) ? null : workbenchLiveKafkaAssistantText(event); return { diff --git a/web/hwlab-cloud-web/src/stores/workbench-server-state.ts b/web/hwlab-cloud-web/src/stores/workbench-server-state.ts index 9360cde2..45b9252e 100644 --- a/web/hwlab-cloud-web/src/stores/workbench-server-state.ts +++ b/web/hwlab-cloud-web/src/stores/workbench-server-state.ts @@ -134,24 +134,23 @@ function reduceSessionDetail(state: WorkbenchServerState, session: WorkbenchSess function reduceSessionList(state: WorkbenchServerState, sessions: WorkbenchSessionRecord[]): WorkbenchServerState { const sessionIds = sessions.map((session) => session.sessionId).filter((sessionId): sessionId is string => Boolean(sessionId)); - const next = sessions.reduce((current, session) => reduceSessionListItem(current, session), state); - return { ...next, sessionOrder: unique(sessionIds) }; -} - -function reduceSessionListItem(state: WorkbenchServerState, session: WorkbenchSessionRecord): WorkbenchServerState { - const sessionId = session.sessionId; - if (!sessionId) return state; - const existing = state.sessionsById[sessionId]; - const existingMessages = state.messagesBySessionId[sessionId] ?? existing?.messages; - const messages = Array.isArray(existingMessages) ? existingMessages : Array.isArray(session.messages) ? session.messages : []; - const merged = mergeSessionRecord(existing, { ...session, messages }); - const sessionStatus = sessionStatusAuthorityFromDetail(session); + const sessionsById = { ...state.sessionsById }; + const sessionStatusById = { ...state.sessionStatusById }; + for (const session of sessions) { + const sessionId = session.sessionId; + if (!sessionId) continue; + const existing = sessionsById[sessionId]; + const existingMessages = state.messagesBySessionId[sessionId] ?? existing?.messages; + const messages = Array.isArray(existingMessages) ? existingMessages : Array.isArray(session.messages) ? session.messages : []; + sessionsById[sessionId] = mergeSessionRecord(existing, { ...session, messages }); + const sessionStatus = sessionStatusAuthorityFromDetail(session); + if (sessionStatus) sessionStatusById[sessionId] = mergeSessionStatusAuthority(sessionStatusById[sessionId], sessionStatus); + } return { ...state, - sessionsById: { ...state.sessionsById, [sessionId]: merged }, - sessionStatusById: sessionStatus - ? { ...state.sessionStatusById, [sessionId]: mergeSessionStatusAuthority(state.sessionStatusById[sessionId], sessionStatus) } - : state.sessionStatusById + sessionOrder: unique(sessionIds), + sessionsById, + sessionStatusById }; } diff --git a/web/hwlab-cloud-web/src/stores/workbench-session.ts b/web/hwlab-cloud-web/src/stores/workbench-session.ts index 08d5eaae..40f0d381 100644 --- a/web/hwlab-cloud-web/src/stores/workbench-session.ts +++ b/web/hwlab-cloud-web/src/stores/workbench-session.ts @@ -146,13 +146,14 @@ export function shouldShowSessionDetailLoading(input: { export function sessionToSessionTab(session: WorkbenchSessionRecord, activeSessionId: string | null, sessionStatusAuthority: SessionStatusAuthorityMap = {}): SessionTab { const sessionId = session.sessionId; - const updatedAt = sessionDisplayUpdatedAt(session); - const latestAgent = latestAgentMessage(session.messages); + const messageProjection = projectSessionTabMessages(session.messages); + const updatedAt = sessionDisplayUpdatedAtWithUserMessage(session, messageProjection.latestUserMessageAt); + const latestAgent = messageProjection.latestAgent; const status = resolveSessionTabStatus(session, sessionStatusAuthority[sessionId], latestAgent); const projection = projectionFromSession(session, sessionStatusAuthority[sessionId]); const projectionText = projectionDiagnosticText(projection); - const trace = firstNonEmptyString(latestAgent?.traceId, latestAgent?.runnerTrace?.traceId, session.messages?.at(-1)?.traceId, session.lastTraceId); - const userMessage = session.messages?.find((message) => message.role === "user"); + const trace = firstNonEmptyString(latestAgent?.traceId, latestAgent?.runnerTrace?.traceId, messageProjection.lastMessageTraceId, session.lastTraceId); + const userMessage = messageProjection.firstUser; const running = isRunningIndicatorStatus(status); const preview = firstReadableSentence( session.firstUserMessagePreview, @@ -316,7 +317,11 @@ function mergeSelectedSessionDetail(stable: WorkbenchSessionRecord[], selected: } function latestAgentMessage(messages: ChatMessage[] | undefined): ChatMessage | null { - return [...(messages ?? [])].reverse().find((message) => message.role === "agent") ?? null; + for (let index = (messages?.length ?? 0) - 1; index >= 0; index -= 1) { + const message = messages?.[index]; + if (message?.role === "agent") return message; + } + return null; } function latestMessageForTrace(messages: ChatMessage[], traceId: string): ChatMessage | null { @@ -443,8 +448,12 @@ function firstReadableSentence(...values: unknown[]): string | null { } export function sessionDisplayUpdatedAt(session: WorkbenchSessionRecord): string | null { + return sessionDisplayUpdatedAtWithUserMessage(session, latestUserMessageAtFromMessages(session.messages)); +} + +function sessionDisplayUpdatedAtWithUserMessage(session: WorkbenchSessionRecord, latestUserMessageAt: string | null): string | null { return firstNonEmptyString( - latestUserMessageAtFromMessages(session.messages ?? []), + latestUserMessageAt, session.lastUserMessageAt, session.snapshot?.lastUserMessageAt, session.startedAt, @@ -454,22 +463,40 @@ export function sessionDisplayUpdatedAt(session: WorkbenchSessionRecord): string } export function latestUserMessageAtFromMessages(messages: ChatMessage[] | undefined): string | null { - const userMessageTimes = (messages ?? []) - .filter((message) => message.role === "user") - .map((message) => firstNonEmptyString(message.createdAt, message.updatedAt)); - return latestTimestamp(...userMessageTimes); + let latest: { value: string; ms: number } | null = null; + let fallback: string | null = null; + for (const message of messages ?? []) { + if (message.role !== "user") continue; + const value = firstNonEmptyString(message.createdAt, message.updatedAt); + fallback ??= value; + if (!value) continue; + const ms = timestampMs(value); + if (ms && (!latest || ms > latest.ms)) latest = { value, ms }; + } + return latest?.value ?? fallback; } -function latestTimestamp(...values: unknown[]): string | null { - let latest: { value: string; ms: number } | null = null; - for (const value of values) { - const text = firstNonEmptyString(value); - if (!text) continue; - const ms = timestampMs(text); - if (!ms) continue; - if (!latest || ms > latest.ms) latest = { value: text, ms }; +function projectSessionTabMessages(messages: ChatMessage[] | undefined): { latestAgent: ChatMessage | null; firstUser: ChatMessage | null; latestUserMessageAt: string | null; lastMessageTraceId: string | null } { + let latestAgent: ChatMessage | null = null; + let firstUser: ChatMessage | null = null; + let latestUserMessageAt: { value: string; ms: number } | null = null; + let firstUserMessageAt: string | null = null; + for (const message of messages ?? []) { + if (message.role === "agent") latestAgent = message; + if (message.role !== "user") continue; + firstUser ??= message; + const value = firstNonEmptyString(message.createdAt, message.updatedAt); + firstUserMessageAt ??= value; + if (!value) continue; + const ms = timestampMs(value); + if (ms && (!latestUserMessageAt || ms > latestUserMessageAt.ms)) latestUserMessageAt = { value, ms }; } - return latest?.value ?? firstNonEmptyString(...values) ?? null; + return { + latestAgent, + firstUser, + latestUserMessageAt: latestUserMessageAt?.value ?? firstUserMessageAt, + lastMessageTraceId: firstNonEmptyString(messages?.at(-1)?.traceId) + }; } function readableSentence(value: unknown): string | null {