mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-13 19:46:16 +03:00
fix(app): show unreachable server state for session tabs (#56248)
Co-authored-by: neriousy <34747899+neriousy@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
141a155906
commit
f0f196b308
@@ -11,7 +11,7 @@ import { ServerConnection } from "@/runtime/server/registry"
|
||||
import { useTabs, type DraftTab } from "@/shell/tabs/tabs"
|
||||
import { SessionUIProvider } from "@/shell/routes/session-ui-provider"
|
||||
import NewSession from "@/new-session/screen"
|
||||
import { IncompatibleServerPanel } from "@/session/incompatible-server-panel"
|
||||
import { ServerUnavailablePanel } from "@/session/server-unavailable-panel"
|
||||
import { ErrorPage } from "@/shell/errors/error"
|
||||
import { SessionPanelFrame, SessionRouteFrame } from "@/session/session-frame"
|
||||
|
||||
@@ -57,7 +57,8 @@ function ResolvedDraftContent(props: { draft: DraftTab }) {
|
||||
fallback={
|
||||
<SessionRouteFrame padded>
|
||||
<SessionPanelFrame raised>
|
||||
<IncompatibleServerPanel
|
||||
<ServerUnavailablePanel
|
||||
reason="incompatible"
|
||||
onClose={() => {
|
||||
const index = tabs.store.findIndex((tab) => tab.type === "draft" && tab.draftID === props.draft.draftID)
|
||||
|
||||
|
||||
@@ -538,6 +538,9 @@ export const dict = {
|
||||
"session.error.incompatible": "This server version isn't supported",
|
||||
"session.error.incompatible.description":
|
||||
"{{server}} is running OpenCode {{version}}, which isn't compatible with this app. Upgrade the server to OpenCode V2 to continue.",
|
||||
"session.error.unreachable": "Can't reach this server",
|
||||
"session.error.unreachable.description":
|
||||
"{{server}} isn't responding, so this session can't load. It will open automatically once the server is reachable again.",
|
||||
"session.background.moveRunning": "Move to background",
|
||||
"session.background.moveInline": "Press {{keybind}} to move running work to the background",
|
||||
"command.session.background": "Move to background",
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useServer } from "@/runtime/server/current"
|
||||
import { ServerConnection, serverName, useServers } from "@/runtime/server/registry"
|
||||
import { useTabs } from "@/shell/tabs/tabs"
|
||||
import { isLocalSessionNotFoundError, isSessionNotFoundError } from "@/runtime/server/errors"
|
||||
import { IncompatibleServerPanel } from "./incompatible-server-panel"
|
||||
import { ServerUnavailablePanel } from "./server-unavailable-panel"
|
||||
|
||||
export function SessionErrorFallback(props: { error: unknown; sessionID?: string; serverKey?: ServerConnection.Key }) {
|
||||
const language = useLanguage()
|
||||
@@ -28,10 +28,14 @@ export function SessionErrorFallback(props: { error: unknown; sessionID?: string
|
||||
return (
|
||||
<Show
|
||||
when={!activeServer.health?.incompatible}
|
||||
fallback={<IncompatibleServerPanel onClose={props.sessionID ? closeSession : undefined} />}
|
||||
fallback={<ServerUnavailablePanel reason="incompatible" onClose={props.sessionID ? closeSession : undefined} />}
|
||||
>
|
||||
<Show
|
||||
when={isCurrentSessionNotFoundError(props.error, props.sessionID)}
|
||||
when={
|
||||
props.sessionID &&
|
||||
(isSessionNotFoundError(props.error, props.sessionID) ||
|
||||
isLocalSessionNotFoundError(props.error, props.sessionID))
|
||||
}
|
||||
fallback={<ErrorPage error={props.error} />}
|
||||
>
|
||||
<div class="flex-1 min-h-0 overflow-hidden">
|
||||
@@ -69,9 +73,3 @@ export function SessionErrorFallback(props: { error: unknown; sessionID?: string
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
function isCurrentSessionNotFoundError(error: unknown, sessionID: string | undefined) {
|
||||
if (!sessionID) return false
|
||||
|
||||
return isSessionNotFoundError(error, sessionID) || isLocalSessionNotFoundError(error, sessionID)
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import { requireServerKey } from "@/shell/routes/session"
|
||||
import { useSessionModel } from "./model"
|
||||
import { SessionPanelFrame } from "./session-frame"
|
||||
import { SessionIdentityHeader } from "./session-identity-header"
|
||||
import { IncompatibleServerPanel } from "./incompatible-server-panel"
|
||||
import { ServerUnavailablePanel } from "./server-unavailable-panel"
|
||||
import { SessionErrorFallback } from "./route-error"
|
||||
import { createSessionResolution } from "./session-resolution"
|
||||
import { SessionScreenView } from "./screen"
|
||||
@@ -128,18 +128,39 @@ function ResolvedTargetSessionRoute() {
|
||||
|
||||
const directory = createMemo(() => current()?.location.directory)
|
||||
|
||||
// Only a failed health check: a version mismatch has its own panel, and a rejected credential is not an outage.
|
||||
const unreachable = () => {
|
||||
const health = server.health
|
||||
|
||||
return !!health && !health.healthy && !health.checking && !health.unauthorized && !health.incompatible
|
||||
}
|
||||
|
||||
return (
|
||||
<Show
|
||||
when={!server.health?.incompatible}
|
||||
fallback={
|
||||
<SessionStatePanel>
|
||||
<IncompatibleServerPanel
|
||||
<ServerUnavailablePanel
|
||||
reason="incompatible"
|
||||
onClose={() => tabs.removeSessionTab({ server: server.key, sessionId: params.id })}
|
||||
/>
|
||||
</SessionStatePanel>
|
||||
}
|
||||
>
|
||||
<Show when={directory()} fallback={<PendingSessionState sessionID={params.id} />}>
|
||||
<Show
|
||||
when={directory()}
|
||||
fallback={
|
||||
// Nothing is cached and an unreachable server never resolves the session, so explain instead of waiting blank.
|
||||
<Show when={unreachable()} fallback={<PendingSessionState sessionID={params.id} />}>
|
||||
<SessionStatePanel>
|
||||
<ServerUnavailablePanel
|
||||
reason="unreachable"
|
||||
onClose={() => tabs.removeSessionTab({ server: server.key, sessionId: params.id })}
|
||||
/>
|
||||
</SessionStatePanel>
|
||||
</Show>
|
||||
}
|
||||
>
|
||||
{(value) => (
|
||||
<LocationProvider directory={value()}>
|
||||
<SessionUIProvider directory={value()} server={server.key}>
|
||||
|
||||
+12
-6
@@ -5,7 +5,7 @@ import { useLanguage } from "@/runtime/i18n/language"
|
||||
import { useServer } from "@/runtime/server/current"
|
||||
import { serverName } from "@/runtime/server/registry"
|
||||
|
||||
export function IncompatibleServerPanel(props: { onClose?: () => void }) {
|
||||
export function ServerUnavailablePanel(props: { reason: "incompatible" | "unreachable"; onClose?: () => void }) {
|
||||
const language = useLanguage()
|
||||
const server = useServer()
|
||||
|
||||
@@ -14,12 +14,18 @@ export function IncompatibleServerPanel(props: { onClose?: () => void }) {
|
||||
<div class="h-full px-6 pb-42 -mt-4 flex flex-col items-center justify-center text-center gap-4">
|
||||
<Icon name="warning" size="large" class="text-icon-warning-base" />
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<div class="text-16-medium text-text max-w-md">{language.t("session.error.incompatible")}</div>
|
||||
<div class="text-16-medium text-text max-w-md">
|
||||
{props.reason === "incompatible"
|
||||
? language.t("session.error.incompatible")
|
||||
: language.t("session.error.unreachable")}
|
||||
</div>
|
||||
<div class="text-13-regular text-text-weak max-w-md">
|
||||
{language.t("session.error.incompatible.description", {
|
||||
server: serverName(server.conn),
|
||||
version: server.health?.version ?? "1",
|
||||
})}
|
||||
{props.reason === "incompatible"
|
||||
? language.t("session.error.incompatible.description", {
|
||||
server: serverName(server.conn),
|
||||
version: server.health?.version ?? "1",
|
||||
})
|
||||
: language.t("session.error.unreachable.description", { server: serverName(server.conn) })}
|
||||
</div>
|
||||
</div>
|
||||
<Show when={props.onClose}>
|
||||
@@ -30,6 +30,7 @@ export function TabNavItem(props: {
|
||||
unread: boolean
|
||||
loading: boolean
|
||||
fallbackTitle?: string
|
||||
fallbackDirectory?: string
|
||||
onRename: (title: string) => Promise<void>
|
||||
onClose: () => void
|
||||
onNavigate: () => void
|
||||
@@ -63,9 +64,11 @@ export function TabNavItem(props: {
|
||||
const project = createMemo(() => {
|
||||
const session = props.session
|
||||
|
||||
if (!session) return
|
||||
if (session) return serverCtx()?.projects.forSession(session)
|
||||
|
||||
return serverCtx()?.projects.forSession(session)
|
||||
if (!props.fallbackDirectory) return
|
||||
|
||||
return serverCtx()?.projects.resolve({ worktree: props.fallbackDirectory, expanded: false })
|
||||
})
|
||||
|
||||
const title = createMemo(() => {
|
||||
@@ -295,7 +298,25 @@ export function TabNavItem(props: {
|
||||
<Show
|
||||
when={props.preparing}
|
||||
fallback={
|
||||
<span class="block size-4 rounded-[3px] border border-v2-border-border-muted" aria-hidden="true" />
|
||||
// The remembered directory keeps the project avatar while the server is unreachable.
|
||||
<Show
|
||||
when={props.fallbackDirectory}
|
||||
fallback={
|
||||
<span
|
||||
class="block size-4 rounded-[3px] border border-v2-border-border-muted"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
}
|
||||
>
|
||||
{(directory) => (
|
||||
<SessionTabAvatarView
|
||||
project={project()}
|
||||
directory={directory()}
|
||||
unread={false}
|
||||
loading={false}
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
}
|
||||
>
|
||||
<SessionProgressIndicatorV2 />
|
||||
|
||||
@@ -31,6 +31,7 @@ function SessionTabSlot(props: {
|
||||
unread: boolean
|
||||
loading: boolean
|
||||
fallbackTitle?: string
|
||||
fallbackDirectory?: string
|
||||
onRename: (title: string) => Promise<void>
|
||||
onNavigate: (element: HTMLDivElement) => void
|
||||
onClose: () => void
|
||||
@@ -70,6 +71,7 @@ function SessionTabSlot(props: {
|
||||
unread={props.unread}
|
||||
loading={props.loading}
|
||||
fallbackTitle={props.fallbackTitle}
|
||||
fallbackDirectory={props.fallbackDirectory}
|
||||
onRename={props.onRename}
|
||||
onNavigate={() => props.onNavigate(ref)}
|
||||
onClose={props.onClose}
|
||||
@@ -209,6 +211,7 @@ function SessionTabEntry(props: {
|
||||
? language.t("session.tab.session")
|
||||
: (persisted()?.title ?? (missingSession() ? language.t("session.tab.unknown") : undefined))
|
||||
}
|
||||
fallbackDirectory={persisted()?.directory}
|
||||
onRename={rename}
|
||||
onNavigate={props.onNavigate}
|
||||
onClose={props.onClose}
|
||||
|
||||
Reference in New Issue
Block a user