mirror of
https://github.com/NeuralNomadsAI/CodeNomad.git
synced 2026-07-09 16:00:52 +00:00
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:
parent
8bcf365fc3
commit
ff10c1f395
15 changed files with 139 additions and 2 deletions
|
|
@ -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) => (
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": "הפעל עצירה לתגובות עוזר ארוכות",
|
||||
|
|
|
|||
|
|
@ -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": "長いアシスタント返信の保持を有効にする",
|
||||
|
|
|
|||
|
|
@ -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": "लामो प्रतिक्रियाहरूको लागि होल्ड सक्षम गर्नुहोस्",
|
||||
|
|
|
|||
|
|
@ -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": "Включить удержание для длинных ответов ассистента",
|
||||
|
|
|
|||
|
|
@ -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": "启用长助手回复保持",
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {
|
|||
getSessionDraftPrompt,
|
||||
getSessionFamily,
|
||||
getSessionInfo,
|
||||
getSessionMessagesLoadError,
|
||||
getSessionSearchQuery,
|
||||
getSessionSearchThreads,
|
||||
getSessionThreads,
|
||||
|
|
@ -134,6 +135,7 @@ export {
|
|||
getSessionDraftPrompt,
|
||||
getSessionFamily,
|
||||
getSessionInfo,
|
||||
getSessionMessagesLoadError,
|
||||
getSessionSearchQuery,
|
||||
getSessionSearchThreads,
|
||||
getSessionThreads,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue