fix(ui): stop message load retry loop (#529)

## Summary
- store per-session message load failures so automatic session effects
stop retrying after server errors
- show the server-provided error in the message stream with a reload
action that forces a fresh request
- add localized labels for the load-error title and reload button

## Validation
- npm run typecheck --workspace @codenomad/ui
- npm run build --workspace @codenomad/ui
This commit is contained in:
Shantur Rathore 2026-06-07 22:23:00 +01:00 committed by GitHub
parent 8bcf365fc3
commit ff10c1f395
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 139 additions and 2 deletions

View file

@ -34,6 +34,7 @@ export interface MessageSectionProps {
instanceId: string
sessionId: string
loading?: boolean
loadError?: string | null
emptyStateVariant?: "messages" | "no-session"
onRevert?: (messageId: string) => void
onDeleteMessagesUpTo?: (messageId: string) => void | Promise<void>
@ -43,6 +44,7 @@ export interface MessageSectionProps {
onSidebarToggle?: () => void
forceCompactStatusLayout?: boolean
onQuoteSelection?: (text: string, mode: "quote" | "code") => void
onReloadMessages?: () => void
isActive?: boolean
sessionStreamingActive?: boolean
}
@ -1421,7 +1423,7 @@ export default function MessageSection(props: MessageSectionProps) {
)}
renderBeforeItems={() => (
<>
<Show when={!props.loading && visibleMessageIds().length === 0}>
<Show when={!props.loading && !props.loadError && visibleMessageIds().length === 0}>
<Show
when={emptyStateVariant() === "no-session"}
fallback={
@ -1466,6 +1468,20 @@ export default function MessageSection(props: MessageSectionProps) {
<p>{t("messageSection.loading.messages")}</p>
</div>
</Show>
<Show when={!props.loading && props.loadError}>
{(loadError) => (
<div class="message-load-error-state">
<div class="message-load-error-card">
<h3>{t("messageSection.loadError.title")}</h3>
<p>{loadError()}</p>
<button type="button" class="message-load-error-retry" onClick={() => props.onReloadMessages?.()}>
{t("messageSection.loadError.reload")}
</button>
</div>
</div>
)}
</Show>
</>
)}
renderItem={(messageId, index) => (

View file

@ -8,7 +8,7 @@ import PromptInput from "../prompt-input"
import PromptAttachmentsBar from "../prompt-input/PromptAttachmentsBar"
import { getAttachments, removeAttachment } from "../../stores/attachments"
import { instances } from "../../stores/instances"
import { loadMessages, sendMessage, forkSession, renameSession, isSessionMessagesLoading, markSessionIdleSeen, setActiveParentSession, setActiveSession, runShellCommand, abortSession } from "../../stores/sessions"
import { loadMessages, sendMessage, forkSession, renameSession, isSessionMessagesLoading, getSessionMessagesLoadError, markSessionIdleSeen, setActiveParentSession, setActiveSession, runShellCommand, abortSession } from "../../stores/sessions"
import { clearSessionIdleFade, IDLE_STATUS_VISIBILITY_MS, getSessionStatus, isSessionBusy as getSessionBusyStatus, markSessionIdleFadeStarted } from "../../stores/session-status"
import { deleteMessage } from "../../stores/session-actions"
import { showAlertDialog } from "../../stores/alerts"
@ -47,6 +47,7 @@ export const SessionView: Component<SessionViewProps> = (props) => {
const { preferences } = useConfig()
const session = () => props.activeSessions.get(props.sessionId)
const messagesLoading = createMemo(() => isSessionMessagesLoading(props.instanceId, props.sessionId))
const messagesLoadError = createMemo(() => getSessionMessagesLoadError(props.instanceId, props.sessionId))
const messageStore = createMemo(() => messageStoreBus.getOrCreate(props.instanceId))
const sessionBusy = createMemo(() => {
const currentSession = session()
@ -207,6 +208,14 @@ export const SessionView: Component<SessionViewProps> = (props) => {
}
})
function handleReloadMessages() {
const currentSession = session()
if (!currentSession) return
loadMessages(props.instanceId, currentSession.id, { force: true }).catch((error) =>
log.error("Failed to reload messages", error),
)
}
createEffect(
on(
() => messageStore().getSessionMessageIds(props.sessionId).length,
@ -436,6 +445,8 @@ export const SessionView: Component<SessionViewProps> = (props) => {
instanceId={props.instanceId}
sessionId={activeSession.id}
loading={messagesLoading()}
loadError={messagesLoadError()}
onReloadMessages={handleReloadMessages}
sessionStreamingActive={sessionStreamingActive()}
onRevert={handleRevert}
onDeleteMessagesUpTo={handleDeleteMessagesUpTo}

View file

@ -16,6 +16,8 @@ export const messagingMessages = {
"messageSection.empty.tips.askAboutCodebase": "Fragen Sie nach Ihrem Code",
"messageSection.empty.tips.attachFilesPrefix": "Dateien anhängen mit",
"messageSection.loading.messages": "Nachrichten werden geladen...",
"messageSection.loadError.title": "Nachrichten konnten nicht geladen werden",
"messageSection.loadError.reload": "Nachrichten neu laden",
"messageSection.scroll.toFirstAriaLabel": "Zur ersten Nachricht scrollen",
"messageSection.scroll.toLatestAriaLabel": "Zur neuesten Nachricht scrollen",
"messageSection.scroll.enableHoldAriaLabel": "Halten für lange Assistentenantworten aktivieren",

View file

@ -16,6 +16,8 @@ export const messagingMessages = {
"messageSection.empty.tips.askAboutCodebase": "Ask about your codebase",
"messageSection.empty.tips.attachFilesPrefix": "Attach files with",
"messageSection.loading.messages": "Loading messages...",
"messageSection.loadError.title": "Could not load messages",
"messageSection.loadError.reload": "Reload messages",
"messageSection.scroll.toFirstAriaLabel": "Scroll to first message",
"messageSection.scroll.toLatestAriaLabel": "Scroll to latest message",
"messageSection.scroll.enableHoldAriaLabel": "Enable hold for long assistant replies",

View file

@ -16,6 +16,8 @@ export const messagingMessages = {
"messageSection.empty.tips.askAboutCodebase": "Pregunta sobre tu codebase",
"messageSection.empty.tips.attachFilesPrefix": "Adjunta archivos con",
"messageSection.loading.messages": "Cargando mensajes...",
"messageSection.loadError.title": "No se pudieron cargar los mensajes",
"messageSection.loadError.reload": "Recargar mensajes",
"messageSection.scroll.toFirstAriaLabel": "Desplazarse al primer mensaje",
"messageSection.scroll.toLatestAriaLabel": "Desplazarse al último mensaje",
"messageSection.scroll.enableHoldAriaLabel": "Activar pausa para respuestas largas del asistente",

View file

@ -16,6 +16,8 @@ export const messagingMessages = {
"messageSection.empty.tips.askAboutCodebase": "Parler de votre codebase",
"messageSection.empty.tips.attachFilesPrefix": "Joindre des fichiers avec",
"messageSection.loading.messages": "Chargement des messages...",
"messageSection.loadError.title": "Impossible de charger les messages",
"messageSection.loadError.reload": "Recharger les messages",
"messageSection.scroll.toFirstAriaLabel": "Aller au premier message",
"messageSection.scroll.toLatestAriaLabel": "Aller au dernier message",
"messageSection.scroll.enableHoldAriaLabel": "Activer le maintien pour les longues réponses de l'assistant",

View file

@ -16,6 +16,8 @@ export const messagingMessages = {
"messageSection.empty.tips.askAboutCodebase": "שאל על בסיס הקוד שלך",
"messageSection.empty.tips.attachFilesPrefix": "צרף קבצים עם",
"messageSection.loading.messages": "טוען הודעות...",
"messageSection.loadError.title": "לא ניתן לטעון הודעות",
"messageSection.loadError.reload": "טען הודעות מחדש",
"messageSection.scroll.toFirstAriaLabel": "גלול להודעה הראשונה",
"messageSection.scroll.toLatestAriaLabel": "גלול להודעה האחרונה",
"messageSection.scroll.enableHoldAriaLabel": "הפעל עצירה לתגובות עוזר ארוכות",

View file

@ -16,6 +16,8 @@ export const messagingMessages = {
"messageSection.empty.tips.askAboutCodebase": "コードベースについて質問",
"messageSection.empty.tips.attachFilesPrefix": "次でファイルを添付:",
"messageSection.loading.messages": "メッセージを読み込み中...",
"messageSection.loadError.title": "メッセージを読み込めませんでした",
"messageSection.loadError.reload": "メッセージを再読み込み",
"messageSection.scroll.toFirstAriaLabel": "最初のメッセージへスクロール",
"messageSection.scroll.toLatestAriaLabel": "最新のメッセージへスクロール",
"messageSection.scroll.enableHoldAriaLabel": "長いアシスタント返信の保持を有効にする",

View file

@ -16,6 +16,8 @@ export const messagingMessages = {
"messageSection.empty.tips.askAboutCodebase": "आफ्नो कोडबेसको बारेमा सोध्नुहोस्",
"messageSection.empty.tips.attachFilesPrefix": "यसको साथ फाइलहरू संलग्न गर्नुहोस्",
"messageSection.loading.messages": "सन्देशहरू लोड गर्दै...",
"messageSection.loadError.title": "सन्देशहरू लोड गर्न सकिएन",
"messageSection.loadError.reload": "सन्देशहरू फेरि लोड गर्नुहोस्",
"messageSection.scroll.toFirstAriaLabel": "पहिलो सन्देशमा जानुहोस्",
"messageSection.scroll.toLatestAriaLabel": "भर्खरको सन्देशमा जानुहोस्",
"messageSection.scroll.enableHoldAriaLabel": "लामो प्रतिक्रियाहरूको लागि होल्ड सक्षम गर्नुहोस्",

View file

@ -16,6 +16,8 @@ export const messagingMessages = {
"messageSection.empty.tips.askAboutCodebase": "Спросите о своей кодовой базе",
"messageSection.empty.tips.attachFilesPrefix": "Прикрепляйте файлы через",
"messageSection.loading.messages": "Загрузка сообщений…",
"messageSection.loadError.title": "Не удалось загрузить сообщения",
"messageSection.loadError.reload": "Перезагрузить сообщения",
"messageSection.scroll.toFirstAriaLabel": "Прокрутить к первому сообщению",
"messageSection.scroll.toLatestAriaLabel": "Прокрутить к последнему сообщению",
"messageSection.scroll.enableHoldAriaLabel": "Включить удержание для длинных ответов ассистента",

View file

@ -16,6 +16,8 @@ export const messagingMessages = {
"messageSection.empty.tips.askAboutCodebase": "询问你的代码库",
"messageSection.empty.tips.attachFilesPrefix": "通过以下方式附加文件",
"messageSection.loading.messages": "正在加载消息...",
"messageSection.loadError.title": "无法加载消息",
"messageSection.loadError.reload": "重新加载消息",
"messageSection.scroll.toFirstAriaLabel": "滚动到第一条消息",
"messageSection.scroll.toLatestAriaLabel": "滚动到最新消息",
"messageSection.scroll.enableHoldAriaLabel": "启用长助手回复保持",

View file

@ -18,11 +18,13 @@ import {
getDescendantSessions,
isBlankSession,
messagesLoaded,
getSessionMessagesLoadError,
pruneDraftPrompts,
providers,
setActiveSessionId,
setAgents,
setMessagesLoaded,
setSessionMessagesLoadError,
setProviders,
setSessionInfoByInstance,
setSessions,
@ -61,6 +63,27 @@ const log = getLogger("api")
const pendingSessionDiffFetches = new Map<string, Promise<void>>()
function getErrorMessage(error: unknown): string {
if (!error) return "Failed to load messages"
const cause = (error as any)?.cause
if (cause && cause !== error) {
const causeMessage = getErrorMessage(cause)
if (causeMessage) return causeMessage
}
const dataMessage = (error as any)?.data?.message
if (typeof dataMessage === "string" && dataMessage.trim()) return dataMessage
const errorMessage = (error as any)?.message
if (typeof errorMessage === "string" && errorMessage.trim()) return errorMessage
const errorText = (error as any)?.error
if (typeof errorText === "string" && errorText.trim()) return errorText
return "Failed to load messages"
}
async function getSessionWorkspacePayload(instanceId: string, sessionId: string): Promise<{ workspace?: string }> {
const workspace = await getOpenCodeWorkspaceIdForSession(instanceId, sessionId)
return workspace ? { workspace } : {}
@ -850,6 +873,11 @@ async function loadMessages(
return
}
const previousError = getSessionMessagesLoadError(instanceId, sessionId)
if (previousError && !force) {
return
}
const isLoading = loading().loadingMessages.get(instanceId)?.has(sessionId)
if (isLoading) {
return
@ -881,6 +909,7 @@ async function loadMessages(
next.loadingMessages.set(instanceId, loadingSet)
return next
})
setSessionMessagesLoadError(instanceId, sessionId, null)
try {
log.info(`[HTTP] GET /session.${"messages"} for instance ${instanceId}`, { sessionId })
@ -893,6 +922,8 @@ async function loadMessages(
return
}
setSessionMessagesLoadError(instanceId, sessionId, null)
// Treat empty sessions as loaded to avoid re-fetch loops.
setMessagesLoaded((prev) => {
const next = new Map(prev)
@ -1000,6 +1031,7 @@ async function loadMessages(
} catch (error) {
log.error("Failed to load messages:", error)
setSessionMessagesLoadError(instanceId, sessionId, getErrorMessage(error))
throw error
} finally {
setLoading((prev) => {

View file

@ -49,6 +49,7 @@ const [loading, setLoading] = createSignal({
})
const [messagesLoaded, setMessagesLoaded] = createSignal<Map<string, Set<string>>>(new Map())
const [messageLoadErrors, setMessageLoadErrors] = createSignal<Map<string, Map<string, string>>>(new Map())
const [sessionInfoByInstance, setSessionInfoByInstance] = createSignal<Map<string, Map<string, SessionInfo>>>(new Map())
const [threadTotalsByInstance, setThreadTotalsByInstance] = createSignal<Map<string, Map<string, ThreadTotals>>>(new Map())
@ -844,6 +845,31 @@ function isSessionMessagesLoading(instanceId: string, sessionId: string): boolea
return Boolean(loading().loadingMessages.get(instanceId)?.has(sessionId))
}
function getSessionMessagesLoadError(instanceId: string, sessionId: string): string | undefined {
return messageLoadErrors().get(instanceId)?.get(sessionId)
}
function setSessionMessagesLoadError(instanceId: string, sessionId: string, error: string | null): void {
setMessageLoadErrors((prev) => {
const next = new Map(prev)
const instanceErrors = new Map(next.get(instanceId))
if (error) {
instanceErrors.set(sessionId, error)
next.set(instanceId, instanceErrors)
return next
}
instanceErrors.delete(sessionId)
if (instanceErrors.size > 0) {
next.set(instanceId, instanceErrors)
} else {
next.delete(instanceId)
}
return next
})
}
function getSessionInfo(instanceId: string, sessionId: string): SessionInfo | undefined {
return sessionInfoByInstance().get(instanceId)?.get(sessionId)
}
@ -995,6 +1021,7 @@ export {
setLoading,
messagesLoaded,
setMessagesLoaded,
setSessionMessagesLoadError,
sessionInfoByInstance,
setSessionInfoByInstance,
threadTotalsByInstance,
@ -1035,6 +1062,7 @@ export {
setActiveSessionFromList,
isSessionBusy,
isSessionMessagesLoading,
getSessionMessagesLoadError,
getSessionInfo,
isBlankSession,
cleanupBlankSessions,

View file

@ -19,6 +19,7 @@ import {
getSessionDraftPrompt,
getSessionFamily,
getSessionInfo,
getSessionMessagesLoadError,
getSessionSearchQuery,
getSessionSearchThreads,
getSessionThreads,
@ -134,6 +135,7 @@ export {
getSessionDraftPrompt,
getSessionFamily,
getSessionInfo,
getSessionMessagesLoadError,
getSessionSearchQuery,
getSessionSearchThreads,
getSessionThreads,

View file

@ -64,3 +64,33 @@
border-top-color: var(--accent-primary);
animation: spin 1s linear infinite;
}
.message-load-error-state {
@apply flex-1 flex items-center justify-center p-12;
}
.message-load-error-card {
@apply max-w-md rounded-xl border p-6 text-center shadow-sm;
background: var(--surface-secondary);
border-color: var(--border-base);
}
.message-load-error-card h3 {
@apply text-base font-semibold;
color: var(--text-primary);
}
.message-load-error-card p {
@apply mt-3 text-sm leading-6 break-words;
color: var(--text-muted);
}
.message-load-error-retry {
@apply mt-5 rounded-md px-4 py-2 text-sm font-medium transition-colors;
background: var(--accent-primary);
color: var(--text-on-accent);
}
.message-load-error-retry:hover {
background: var(--accent-hover);
}