Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions packages/ui/src/components/load-error-state.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { Component } from "solid-js"
import { AlertTriangle } from "lucide-solid"

interface LoadErrorStateProps {
title: string
error: string
retryLabel: string
onRetry: () => void
variant?: "compact" | "panel"
}

const LoadErrorState: Component<LoadErrorStateProps> = (props) => {
const content = (
<div class={props.variant === "compact" ? "flex items-start gap-2" : "flex flex-col items-center text-center"}>
<AlertTriangle
class={props.variant === "compact" ? "mt-0.5 h-4 w-4 flex-shrink-0 text-danger" : "mb-3 h-6 w-6 text-danger"}
aria-hidden="true"
/>
<div class="min-w-0 flex-1">
<h3 class="text-sm font-medium text-danger">{props.title}</h3>
<p class="mt-2 break-words text-xs leading-5 text-muted">{props.error}</p>
<button type="button" class="button-tertiary mt-3" onClick={props.onRetry}>
{props.retryLabel}
</button>
</div>
</div>
)

if (props.variant === "compact") {
return <div class="m-3 border border-danger bg-danger/10 p-3" role="alert">{content}</div>
}

return (
<div class="flex flex-1 items-center justify-center p-12" role="alert">
<div class="w-full max-w-md border border-danger bg-danger/10 p-6">{content}</div>
</div>
)
}

export default LoadErrorState
16 changes: 7 additions & 9 deletions packages/ui/src/components/message-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Show, createEffect, createMemo, createSignal, onCleanup, on, untrack }
import { ArrowUpDown, ChevronDown, ChevronUp, MoreHorizontal, Pause, Search, Trash, X } from "lucide-solid"
import Kbd from "./kbd"
import BrandedEmptyState from "./branded-empty-state"
import LoadErrorState from "./load-error-state"
import MessageBlock from "./message-block"
import { getMessageAnchorId } from "./message-anchors"
import MessageTimeline, { buildTimelineSegments, type TimelineSegment } from "./message-timeline"
Expand Down Expand Up @@ -1583,15 +1584,12 @@ export default function MessageSection(props: MessageSectionProps) {

<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>
<LoadErrorState
title={t("messageSection.loadError.title")}
error={loadError()}
retryLabel={t("messageSection.loadError.reload")}
onRetry={() => props.onReloadMessages?.()}
/>
)}
</Show>
</>
Expand Down
36 changes: 32 additions & 4 deletions packages/ui/src/components/session-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { SessionThread } from "../stores/session-state"
import { getRetrySeconds, getSessionIdleFadeClass, getSessionRetry, getSessionStatus, shouldShowSessionStatus } from "../stores/session-status"
import { Bot, User, Copy, Trash2, Pencil, ShieldAlert, ChevronDown, Search, Square, CheckSquare, MinusSquare, Split, RotateCw } from "lucide-solid"
import KeyboardHint from "./keyboard-hint"
import LoadErrorState from "./load-error-state"
import SessionRenameDialog from "./session-rename-dialog"
import { keyboardRegistry } from "../lib/keyboard-registry"
import { showToastNotification } from "../lib/notifications"
Expand All @@ -23,7 +24,9 @@ import {
loadMoreSessions,
searchSessions,
getSessionHasMore,
getSessionListError,
clearSessionSearch,
fetchSessions,
getSessionSearchQuery,
getSessionSearchThreads,
isSessionSearchLoading,
Expand Down Expand Up @@ -83,6 +86,13 @@ const SessionList: Component<SessionListProps> = (props) => {
const isFetchingSessions = createMemo(() => {
return loading().fetchingSessions.get(props.instanceId) ?? false
})
const sessionListError = createMemo(() => getSessionListError(props.instanceId))

const handleRetrySessions = () => {
void fetchSessions(props.instanceId, { reset: true }).catch((error) => {
log.error("Failed to retry session list:", error)
})
}

createEffect(() => {
const el = sentinelEl()
Expand Down Expand Up @@ -875,17 +885,35 @@ const SessionList: Component<SessionListProps> = (props) => {

<div class="session-list flex-1 overflow-y-auto" ref={(el) => listEl[1](el)}>

<Show when={filteredThreads().length > 0}>
<div class="session-section">
<For each={filteredThreads()}>
<Show when={sessionListError()}>
{(error) => (
<LoadErrorState
variant="compact"
title={t("sessionList.loadError.title")}
error={error()}
retryLabel={t("sessionList.loadError.retry")}
onRetry={handleRetrySessions}
/>
)}
</Show>

<Show when={!sessionListError() && isFetchingSessions() && filteredThreads().length === 0}>
<div class="flex items-center justify-center p-4 text-xs text-muted" role="status">
<span class="animate-pulse">{t("sessionList.loading.initial")}</span>
</div>
</Show>

<Show when={filteredThreads().length > 0}>
<div class="session-section">
<For each={filteredThreads()}>
{(thread, index) => (
<SessionThreadRow
thread={thread}
depth={0}
isLastChild={index() === filteredThreads().length - 1}
/>
)}
</For>
</For>
<Show when={hasMore() || isFetchingSessions()}>
<div
ref={(el) => setSentinelEl(el)}
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/lib/i18n/messages/de/messaging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const messagingMessages = {
"messageSection.empty.tips.attachFilesPrefix": "Dateien anhängen mit",
"messageSection.loading.messages": "Nachrichten werden geladen...",
"messageSection.loadError.title": "Nachrichten konnten nicht geladen werden",
"messageSection.loadError.detail": "OpenCode hat die Sitzungsnachrichten nicht zurückgegeben.",
"messageSection.loadError.reload": "Nachrichten neu laden",
"messageSection.scroll.toFirstAriaLabel": "Zur ersten Nachricht scrollen",
"messageSection.scroll.toLatestAriaLabel": "Zur neuesten Nachricht scrollen",
Expand Down
4 changes: 4 additions & 0 deletions packages/ui/src/lib/i18n/messages/de/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ export const sessionMessages = {
"sessionList.filter.placeholder": "Sitzungen suchen...",
"sessionList.filter.ariaLabel": "Sitzungen suchen",
"sessionList.loading.more": "Weitere Sitzungen werden geladen...",
"sessionList.loading.initial": "Sitzungen werden geladen...",
"sessionList.loadError.title": "Sitzungen konnten nicht geladen werden",
"sessionList.loadError.detail": "OpenCode hat keine Sitzungsliste zurückgegeben.",
"sessionList.loadError.retry": "Erneut versuchen",
"sessionList.selection.selectAllLabel": "Alle auswählen",
"sessionList.selection.selectAllAriaLabel": "Alle Sitzungen auswählen",
"sessionList.selection.clearLabel": "Leeren",
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/lib/i18n/messages/en/messaging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const messagingMessages = {
"messageSection.empty.tips.attachFilesPrefix": "Attach files with",
"messageSection.loading.messages": "Loading messages...",
"messageSection.loadError.title": "Could not load messages",
"messageSection.loadError.detail": "OpenCode did not return the session messages.",
"messageSection.loadError.reload": "Reload messages",
"messageSection.scroll.toFirstAriaLabel": "Scroll to first message",
"messageSection.scroll.toLatestAriaLabel": "Scroll to latest message",
Expand Down
4 changes: 4 additions & 0 deletions packages/ui/src/lib/i18n/messages/en/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ export const sessionMessages = {
"sessionList.filter.placeholder": "Search sessions…",
"sessionList.filter.ariaLabel": "Search sessions",
"sessionList.loading.more": "Loading more sessions…",
"sessionList.loading.initial": "Loading sessions…",
"sessionList.loadError.title": "Unable to load sessions",
"sessionList.loadError.detail": "OpenCode did not return the session list.",
"sessionList.loadError.retry": "Retry",
"sessionList.selection.selectAllLabel": "Select all",
"sessionList.selection.selectAllAriaLabel": "Select all sessions",
"sessionList.selection.clearLabel": "Clear",
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/lib/i18n/messages/es/messaging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const messagingMessages = {
"messageSection.empty.tips.attachFilesPrefix": "Adjunta archivos con",
"messageSection.loading.messages": "Cargando mensajes...",
"messageSection.loadError.title": "No se pudieron cargar los mensajes",
"messageSection.loadError.detail": "OpenCode no devolvió los mensajes de la sesión.",
"messageSection.loadError.reload": "Recargar mensajes",
"messageSection.scroll.toFirstAriaLabel": "Desplazarse al primer mensaje",
"messageSection.scroll.toLatestAriaLabel": "Desplazarse al último mensaje",
Expand Down
4 changes: 4 additions & 0 deletions packages/ui/src/lib/i18n/messages/es/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ export const sessionMessages = {
"sessionList.filter.placeholder": "Buscar sesiones…",
"sessionList.filter.ariaLabel": "Buscar sesiones",
"sessionList.loading.more": "Cargando más sesiones…",
"sessionList.loading.initial": "Cargando sesiones…",
"sessionList.loadError.title": "No se pudieron cargar las sesiones",
"sessionList.loadError.detail": "OpenCode no devolvió la lista de sesiones.",
"sessionList.loadError.retry": "Reintentar",
"sessionList.selection.selectAllLabel": "Seleccionar todo",
"sessionList.selection.selectAllAriaLabel": "Seleccionar todas las sesiones",
"sessionList.selection.clearLabel": "Limpiar",
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/lib/i18n/messages/fr/messaging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const messagingMessages = {
"messageSection.empty.tips.attachFilesPrefix": "Joindre des fichiers avec",
"messageSection.loading.messages": "Chargement des messages...",
"messageSection.loadError.title": "Impossible de charger les messages",
"messageSection.loadError.detail": "OpenCode n'a pas renvoyé les messages de la session.",
"messageSection.loadError.reload": "Recharger les messages",
"messageSection.scroll.toFirstAriaLabel": "Aller au premier message",
"messageSection.scroll.toLatestAriaLabel": "Aller au dernier message",
Expand Down
4 changes: 4 additions & 0 deletions packages/ui/src/lib/i18n/messages/fr/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ export const sessionMessages = {
"sessionList.filter.placeholder": "Rechercher des sessions…",
"sessionList.filter.ariaLabel": "Rechercher des sessions",
"sessionList.loading.more": "Chargement de plus de sessions…",
"sessionList.loading.initial": "Chargement des sessions…",
"sessionList.loadError.title": "Impossible de charger les sessions",
"sessionList.loadError.detail": "OpenCode n'a pas renvoyé la liste des sessions.",
"sessionList.loadError.retry": "Réessayer",
"sessionList.selection.selectAllLabel": "Tout sélectionner",
"sessionList.selection.selectAllAriaLabel": "Sélectionner toutes les sessions",
"sessionList.selection.clearLabel": "Effacer",
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/lib/i18n/messages/he/messaging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const messagingMessages = {
"messageSection.empty.tips.attachFilesPrefix": "צרף קבצים עם",
"messageSection.loading.messages": "טוען הודעות...",
"messageSection.loadError.title": "לא ניתן לטעון הודעות",
"messageSection.loadError.detail": "OpenCode לא החזיר את הודעות הסשן.",
"messageSection.loadError.reload": "טען הודעות מחדש",
"messageSection.scroll.toFirstAriaLabel": "גלול להודעה הראשונה",
"messageSection.scroll.toLatestAriaLabel": "גלול להודעה האחרונה",
Expand Down
4 changes: 4 additions & 0 deletions packages/ui/src/lib/i18n/messages/he/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ export const sessionMessages = {
"sessionList.filter.placeholder": "חפש סשנים…",
"sessionList.filter.ariaLabel": "חפש סשנים",
"sessionList.loading.more": "טוען עוד סשנים…",
"sessionList.loading.initial": "טוען סשנים…",
"sessionList.loadError.title": "לא ניתן לטעון את הסשנים",
"sessionList.loadError.detail": "OpenCode לא החזיר את רשימת הסשנים.",
"sessionList.loadError.retry": "נסה שוב",
"sessionList.selection.selectAllLabel": "בחר הכל",
"sessionList.selection.selectAllAriaLabel": "בחר את כל הסשנים",
"sessionList.selection.clearLabel": "נקה",
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/lib/i18n/messages/ja/messaging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const messagingMessages = {
"messageSection.empty.tips.attachFilesPrefix": "次でファイルを添付:",
"messageSection.loading.messages": "メッセージを読み込み中...",
"messageSection.loadError.title": "メッセージを読み込めませんでした",
"messageSection.loadError.detail": "OpenCode からセッションメッセージが返されませんでした。",
"messageSection.loadError.reload": "メッセージを再読み込み",
"messageSection.scroll.toFirstAriaLabel": "最初のメッセージへスクロール",
"messageSection.scroll.toLatestAriaLabel": "最新のメッセージへスクロール",
Expand Down
4 changes: 4 additions & 0 deletions packages/ui/src/lib/i18n/messages/ja/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ export const sessionMessages = {
"sessionList.filter.placeholder": "セッションを検索…",
"sessionList.filter.ariaLabel": "セッションを検索",
"sessionList.loading.more": "セッションをさらに読み込んでいます…",
"sessionList.loading.initial": "セッションを読み込んでいます…",
"sessionList.loadError.title": "セッションを読み込めません",
"sessionList.loadError.detail": "OpenCode からセッション一覧が返されませんでした。",
"sessionList.loadError.retry": "再試行",
"sessionList.selection.selectAllLabel": "すべて選択",
"sessionList.selection.selectAllAriaLabel": "すべてのセッションを選択",
"sessionList.selection.clearLabel": "クリア",
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/lib/i18n/messages/ne/messaging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const messagingMessages = {
"messageSection.empty.tips.attachFilesPrefix": "यसको साथ फाइलहरू संलग्न गर्नुहोस्",
"messageSection.loading.messages": "सन्देशहरू लोड गर्दै...",
"messageSection.loadError.title": "सन्देशहरू लोड गर्न सकिएन",
"messageSection.loadError.detail": "OpenCode ले सत्रका सन्देशहरू फिर्ता गरेन।",
"messageSection.loadError.reload": "सन्देशहरू फेरि लोड गर्नुहोस्",
"messageSection.scroll.toFirstAriaLabel": "पहिलो सन्देशमा जानुहोस्",
"messageSection.scroll.toLatestAriaLabel": "भर्खरको सन्देशमा जानुहोस्",
Expand Down
4 changes: 4 additions & 0 deletions packages/ui/src/lib/i18n/messages/ne/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ export const sessionMessages = {
"sessionList.filter.placeholder": "सत्रहरू खोज्नुहोस्...",
"sessionList.filter.ariaLabel": "सत्रहरू खोज्नुहोस्",
"sessionList.loading.more": "थप सत्रहरू लोड गर्दै...",
"sessionList.loading.initial": "सत्रहरू लोड गर्दै...",
"sessionList.loadError.title": "सत्रहरू लोड गर्न सकिएन",
"sessionList.loadError.detail": "OpenCode ले सत्र सूची फिर्ता गरेन।",
"sessionList.loadError.retry": "पुन: प्रयास गर्नुहोस्",
"sessionList.selection.selectAllLabel": "सबै चयन गर्नुहोस्",
"sessionList.selection.selectAllAriaLabel": "सबै सत्रहरू चयन गर्नुहोस्",
"sessionList.selection.clearLabel": "सफा गर्नुहोस्",
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/lib/i18n/messages/ru/messaging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const messagingMessages = {
"messageSection.empty.tips.attachFilesPrefix": "Прикрепляйте файлы через",
"messageSection.loading.messages": "Загрузка сообщений…",
"messageSection.loadError.title": "Не удалось загрузить сообщения",
"messageSection.loadError.detail": "OpenCode не вернул сообщения сессии.",
"messageSection.loadError.reload": "Перезагрузить сообщения",
"messageSection.scroll.toFirstAriaLabel": "Прокрутить к первому сообщению",
"messageSection.scroll.toLatestAriaLabel": "Прокрутить к последнему сообщению",
Expand Down
4 changes: 4 additions & 0 deletions packages/ui/src/lib/i18n/messages/ru/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ export const sessionMessages = {
"sessionList.filter.placeholder": "Поиск сессий…",
"sessionList.filter.ariaLabel": "Поиск сессий",
"sessionList.loading.more": "Загрузка дополнительных сессий…",
"sessionList.loading.initial": "Загрузка сессий…",
"sessionList.loadError.title": "Не удалось загрузить сессии",
"sessionList.loadError.detail": "OpenCode не вернул список сессий.",
"sessionList.loadError.retry": "Повторить",
"sessionList.selection.selectAllLabel": "Выбрать все",
"sessionList.selection.selectAllAriaLabel": "Выбрать все сессии",
"sessionList.selection.clearLabel": "Очистить",
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/lib/i18n/messages/zh-Hans/messaging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const messagingMessages = {
"messageSection.empty.tips.attachFilesPrefix": "通过以下方式附加文件",
"messageSection.loading.messages": "正在加载消息...",
"messageSection.loadError.title": "无法加载消息",
"messageSection.loadError.detail": "OpenCode 未返回会话消息。",
"messageSection.loadError.reload": "重新加载消息",
"messageSection.scroll.toFirstAriaLabel": "滚动到第一条消息",
"messageSection.scroll.toLatestAriaLabel": "滚动到最新消息",
Expand Down
4 changes: 4 additions & 0 deletions packages/ui/src/lib/i18n/messages/zh-Hans/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ export const sessionMessages = {
"sessionList.filter.placeholder": "搜索会话…",
"sessionList.filter.ariaLabel": "搜索会话",
"sessionList.loading.more": "正在加载更多会话…",
"sessionList.loading.initial": "正在加载会话…",
"sessionList.loadError.title": "无法加载会话",
"sessionList.loadError.detail": "OpenCode 未返回会话列表。",
"sessionList.loadError.retry": "重试",
"sessionList.selection.selectAllLabel": "全选",
"sessionList.selection.selectAllAriaLabel": "选择所有会话",
"sessionList.selection.clearLabel": "清除",
Expand Down
40 changes: 40 additions & 0 deletions packages/ui/src/lib/opencode-api.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import assert from "node:assert/strict"
import { describe, it } from "node:test"

import { getOpencodeErrorMessage } from "./opencode-api.ts"

describe("getOpencodeErrorMessage", () => {
it("uses the detailed message from a nested SDK error", () => {
const error = new Error("session.list failed")
;(error as Error & { cause: unknown }).cause = {
data: { message: 'Expected DateTime.Utc at ["items"][37]["time"]["archived"]' },
}

assert.equal(
getOpencodeErrorMessage(error, "Unable to load sessions"),
'Expected DateTime.Utc at ["items"][37]["time"]["archived"]',
)
})

it("uses the contextual fallback when no detail is available", () => {
assert.equal(getOpencodeErrorMessage({}, "Unable to load sessions"), "Unable to load sessions")
})

it("keeps a useful outer message when the cause has no detail", () => {
const error = new Error("Network request failed")
;(error as Error & { cause: unknown }).cause = { code: "ECONNRESET" }
assert.equal(getOpencodeErrorMessage(error, "Unable to load sessions"), "Network request failed")
})

it("reads structured response bodies and handles cyclic causes", () => {
assert.equal(
getOpencodeErrorMessage({ body: { message: "Invalid session timestamp" } }, "Unable to load sessions"),
"Invalid session timestamp",
)

const first: { cause?: unknown } = {}
const second: { cause?: unknown } = { cause: first }
first.cause = second
assert.equal(getOpencodeErrorMessage(first, "Unable to load sessions"), "Unable to load sessions")
})
})
Loading
Loading