mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-24 06:33:01 -04:00
feat(app): warn about V1 servers (#43790)
This commit is contained in:
@@ -196,6 +196,7 @@ function HomeServerRow(props: {
|
||||
health: ServerHealth | undefined
|
||||
}) {
|
||||
const healthy = () => !!props.health?.healthy
|
||||
const incompatible = () => !!props.health?.incompatible
|
||||
const canToggle = () => healthy() && props.projectsForServer(props.server).length > 0
|
||||
const contextMenuID = () => serverContextMenuID(props.server)
|
||||
onCleanup(() => {
|
||||
@@ -203,96 +204,107 @@ function HomeServerRow(props: {
|
||||
if (props.contextMenuOpen(id)) props.onSetContextMenuOpen(id, false)
|
||||
})
|
||||
return (
|
||||
<div class="group/server relative flex h-7 min-w-0 items-center rounded-[6px]">
|
||||
<Tooltip
|
||||
appearance="standard"
|
||||
placement="top"
|
||||
class="flex h-7 w-full min-w-0"
|
||||
inactive={!incompatible()}
|
||||
value={props.language.t("server.row.incompatible", { version: props.health?.version ?? "1" })}
|
||||
>
|
||||
<div class="group/server relative flex h-7 w-full min-w-0 items-center rounded-[6px]">
|
||||
<HomeProjectNavButton
|
||||
type="button"
|
||||
class="pr-16 disabled:opacity-60"
|
||||
class="pr-16"
|
||||
classList={{ "opacity-60": !healthy() && !incompatible() }}
|
||||
data-selected={props.selected ? "" : undefined}
|
||||
disabled={!healthy()}
|
||||
onClick={() => props.onFocusServer(props.server)}
|
||||
>
|
||||
<span
|
||||
data-action="home-server-collapse"
|
||||
class={`
|
||||
disabled={!healthy()}
|
||||
onClick={() => props.onFocusServer(props.server)}
|
||||
>
|
||||
<span
|
||||
data-action="home-server-collapse"
|
||||
class={`
|
||||
-ml-0.5 -mr-1.5 inline-flex size-5 shrink-0 items-center justify-center
|
||||
rounded-[4px] text-v2-icon-icon-muted
|
||||
`}
|
||||
classList={{
|
||||
"hover:bg-v2-overlay-simple-overlay-hover": canToggle(),
|
||||
"cursor-default opacity-40": !canToggle(),
|
||||
}}
|
||||
aria-label={
|
||||
props.collapsed ? props.language.t("home.server.expand") : props.language.t("home.server.collapse")
|
||||
}
|
||||
aria-disabled={!canToggle()}
|
||||
aria-expanded={canToggle() ? !props.collapsed : undefined}
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
if (!canToggle()) return
|
||||
props.onToggleCollapsed(props.server)
|
||||
}}
|
||||
onPointerDown={(event) => event.preventDefault()}
|
||||
>
|
||||
<Icon
|
||||
name="chevron-down"
|
||||
size="small"
|
||||
class="transition-transform duration-150 ease-in-out"
|
||||
style={{ transform: `rotate(${props.collapsed ? -90 : 0}deg)` }}
|
||||
/>
|
||||
</span>
|
||||
<div class="flex size-4 shrink-0 items-center justify-center -mr-0.5">
|
||||
<ServerHealthIndicator health={props.health} />
|
||||
</div>
|
||||
<span class="flex min-w-0 items-center gap-1">
|
||||
<span class={HOME_PROJECT_NAV_LABEL}>{props.server.displayName ?? new URL(props.server.http.url).host}</span>
|
||||
<Show when={props.server.label}>
|
||||
{(label) => (
|
||||
<span
|
||||
class={`
|
||||
classList={{
|
||||
"hover:bg-v2-overlay-simple-overlay-hover": canToggle(),
|
||||
"cursor-default opacity-40": !canToggle(),
|
||||
}}
|
||||
aria-label={
|
||||
props.collapsed ? props.language.t("home.server.expand") : props.language.t("home.server.collapse")
|
||||
}
|
||||
aria-disabled={!canToggle()}
|
||||
aria-expanded={canToggle() ? !props.collapsed : undefined}
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
if (!canToggle()) return
|
||||
props.onToggleCollapsed(props.server)
|
||||
}}
|
||||
onPointerDown={(event) => event.preventDefault()}
|
||||
>
|
||||
<Icon
|
||||
name="chevron-down"
|
||||
size="small"
|
||||
class="transition-transform duration-150 ease-in-out"
|
||||
style={{ transform: `rotate(${props.collapsed || !canToggle() ? -90 : 0}deg)` }}
|
||||
/>
|
||||
</span>
|
||||
<div class="flex size-4 shrink-0 items-center justify-center -mr-0.5">
|
||||
<ServerHealthIndicator health={props.health} />
|
||||
</div>
|
||||
<span class="flex min-w-0 items-center gap-1">
|
||||
<span class={HOME_PROJECT_NAV_LABEL}>
|
||||
{props.server.displayName ?? new URL(props.server.http.url).host}
|
||||
</span>
|
||||
<Show when={props.server.label}>
|
||||
{(label) => (
|
||||
<span
|
||||
class={`
|
||||
shrink-0 rounded-[3px] border border-v2-border-border-base px-1 py-0.5
|
||||
text-[9px] leading-none text-v2-text-text-muted
|
||||
`}
|
||||
>
|
||||
{label()}
|
||||
</span>
|
||||
)}
|
||||
</Show>
|
||||
</span>
|
||||
</HomeProjectNavButton>
|
||||
<div
|
||||
class={`
|
||||
>
|
||||
{label()}
|
||||
</span>
|
||||
)}
|
||||
</Show>
|
||||
</span>
|
||||
</HomeProjectNavButton>
|
||||
<div
|
||||
class={`
|
||||
hover-reveal absolute right-1 top-1/2 flex -translate-y-1/2 items-center gap-1
|
||||
group-hover/server:opacity-100 focus-within:opacity-100 data-[menu=true]:opacity-100
|
||||
`}
|
||||
data-menu={props.contextMenuOpen(contextMenuID())}
|
||||
>
|
||||
<ServerRowMenuView
|
||||
server={props.server}
|
||||
labels={serverMenuLabels(props.language)}
|
||||
canDefault={props.canDefaultServer}
|
||||
isDefault={props.defaultServerKey === ServerConnection.key(props.server)}
|
||||
canRemove={props.canRemoveServer(props.server)}
|
||||
onEdit={props.onEditServer}
|
||||
onSetDefault={() => props.onSetDefaultServer(props.server)}
|
||||
onRemoveDefault={() => props.onSetDefaultServer(undefined)}
|
||||
onRemove={() => props.onRemoveServer(props.server)}
|
||||
open={props.contextMenuOpen(contextMenuID())}
|
||||
onOpenChange={(open) => props.onSetContextMenuOpen(contextMenuID(), open)}
|
||||
/>
|
||||
<Tooltip class="flex shrink-0 items-center" placement="bottom" value={props.language.t("home.project.add")}>
|
||||
<IconButton
|
||||
data-action="home-add-project"
|
||||
variant="ghost-muted"
|
||||
size="small"
|
||||
icon={<Icon name="folder-add-left" />}
|
||||
aria-label={props.language.t("home.project.add")}
|
||||
disabled={props.health?.healthy === false}
|
||||
onClick={() => props.onChooseProject(props.server)}
|
||||
data-menu={props.contextMenuOpen(contextMenuID())}
|
||||
>
|
||||
<ServerRowMenuView
|
||||
server={props.server}
|
||||
labels={serverMenuLabels(props.language)}
|
||||
canDefault={props.canDefaultServer}
|
||||
isDefault={props.defaultServerKey === ServerConnection.key(props.server)}
|
||||
canRemove={props.canRemoveServer(props.server)}
|
||||
onEdit={props.onEditServer}
|
||||
onSetDefault={() => props.onSetDefaultServer(props.server)}
|
||||
onRemoveDefault={() => props.onSetDefaultServer(undefined)}
|
||||
onRemove={() => props.onRemoveServer(props.server)}
|
||||
open={props.contextMenuOpen(contextMenuID())}
|
||||
onOpenChange={(open) => props.onSetContextMenuOpen(contextMenuID(), open)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip class="flex shrink-0 items-center" placement="bottom" value={props.language.t("home.project.add")}>
|
||||
<IconButton
|
||||
data-action="home-add-project"
|
||||
variant="ghost-muted"
|
||||
size="small"
|
||||
icon={<Icon name="folder-add-left" />}
|
||||
aria-label={props.language.t("home.project.add")}
|
||||
disabled={props.health?.healthy === false}
|
||||
onClick={() => props.onChooseProject(props.server)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,11 +6,13 @@ import { useGlobal } from "@/runtime/server/runtime"
|
||||
import { LocationProvider } from "@/workspaces/location"
|
||||
import { ModelsProvider } from "@/providers/models/models"
|
||||
import { ComposerPersistenceProvider } from "@/composer/persistence"
|
||||
import { ServerProvider } from "@/runtime/server/current"
|
||||
import { ServerProvider, useServer } from "@/runtime/server/current"
|
||||
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 { SessionPanelFrame, SessionRouteFrame } from "@/session/session-frame"
|
||||
|
||||
export function DraftRoute() {
|
||||
const [search] = useSearchParams<{ draftId?: string }>()
|
||||
@@ -35,15 +37,7 @@ function ResolvedDraftRoute(props: { draft: DraftTab }) {
|
||||
<Show when={conn()} keyed>
|
||||
{(conn) => (
|
||||
<ServerProvider conn={conn}>
|
||||
<ModelsProvider directory={props.draft.directory}>
|
||||
<LocationProvider directory={props.draft.directory}>
|
||||
<SessionUIProvider directory={props.draft.directory} server={props.draft.server}>
|
||||
<DraftProviders>
|
||||
<NewSession draftId={props.draft.draftID} />
|
||||
</DraftProviders>
|
||||
</SessionUIProvider>
|
||||
</LocationProvider>
|
||||
</ModelsProvider>
|
||||
<ResolvedDraftContent draft={props.draft} />
|
||||
</ServerProvider>
|
||||
)}
|
||||
</Show>
|
||||
@@ -51,6 +45,39 @@ function ResolvedDraftRoute(props: { draft: DraftTab }) {
|
||||
)
|
||||
}
|
||||
|
||||
function ResolvedDraftContent(props: { draft: DraftTab }) {
|
||||
const server = useServer()
|
||||
const tabs = useTabs()
|
||||
|
||||
return (
|
||||
<Show
|
||||
when={!server.health?.incompatible}
|
||||
fallback={
|
||||
<SessionRouteFrame padded>
|
||||
<SessionPanelFrame raised>
|
||||
<IncompatibleServerPanel
|
||||
onClose={() => {
|
||||
const index = tabs.store.findIndex((tab) => tab.type === "draft" && tab.draftID === props.draft.draftID)
|
||||
if (index !== -1) tabs.closeTab(index)
|
||||
}}
|
||||
/>
|
||||
</SessionPanelFrame>
|
||||
</SessionRouteFrame>
|
||||
}
|
||||
>
|
||||
<ModelsProvider directory={props.draft.directory}>
|
||||
<LocationProvider directory={props.draft.directory}>
|
||||
<SessionUIProvider directory={props.draft.directory} server={props.draft.server}>
|
||||
<DraftProviders>
|
||||
<NewSession draftId={props.draft.draftID} />
|
||||
</DraftProviders>
|
||||
</SessionUIProvider>
|
||||
</LocationProvider>
|
||||
</ModelsProvider>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
// The draft page only renders the prompt composer, so it drops TerminalProvider.
|
||||
// FileProvider and CommentsProvider stay because Composer uses file search and comment context.
|
||||
function DraftProviders(props: ParentProps) {
|
||||
|
||||
@@ -444,6 +444,8 @@ export const dict = {
|
||||
"wsl.onboarding.adding": "Adding...",
|
||||
|
||||
"server.row.noUsername": "no username",
|
||||
"server.row.incompatible":
|
||||
"This server is running OpenCode {{version}}, which isn't compatible with this app. Upgrade it to OpenCode V2 to continue.",
|
||||
|
||||
"dialog.project.edit.title": "Edit project",
|
||||
"dialog.project.edit.name": "Name",
|
||||
@@ -650,6 +652,9 @@ export const dict = {
|
||||
"session.error.notFound": "This session cannot be found",
|
||||
"session.error.notFound.description": "This tab points to a session that no longer exists on this server.",
|
||||
"session.error.notFound.closeTab": "Close Tab",
|
||||
"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.background.moveTasks": "Move {{tasks}} to background",
|
||||
"session.background.inBackground": "Running {{tasks}} in background",
|
||||
"session.background.combine": "{{first}} and {{second}}",
|
||||
|
||||
@@ -29,6 +29,23 @@ describe("checkServerHealth", () => {
|
||||
expect(request?.pathname).toBe("/api/health")
|
||||
})
|
||||
|
||||
test("identifies a V1 server without a version as incompatible", async () => {
|
||||
const requests: string[] = []
|
||||
const fetch = (async (input: RequestInfo | URL) => {
|
||||
const url = input instanceof URL ? input : new URL(input instanceof Request ? input.url : input)
|
||||
requests.push(url.pathname)
|
||||
return new Response(JSON.stringify(url.pathname === "/global/health" ? { version: "1.18.15" } : { healthy: true }), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
})
|
||||
}) as unknown as typeof globalThis.fetch
|
||||
|
||||
const result = await checkServerHealth(server, fetch)
|
||||
|
||||
expect(result).toEqual({ healthy: false, version: "1.18.15", incompatible: true })
|
||||
expect(requests).toEqual(["/api/health", "/global/health"])
|
||||
})
|
||||
|
||||
test("allows slow servers thirty seconds by default", async () => {
|
||||
const timeout = Object.getOwnPropertyDescriptor(AbortSignal, "timeout")
|
||||
let timeoutMs = 0
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ClientError, OpenCode } from "@opencode-ai/client"
|
||||
import { Accessor, createEffect, onCleanup } from "solid-js"
|
||||
import { createStore, reconcile } from "solid-js/store"
|
||||
|
||||
export type ServerHealth = { healthy: boolean; version?: string }
|
||||
export type ServerHealth = { healthy: boolean; version?: string; incompatible?: boolean }
|
||||
|
||||
interface CheckServerHealthOptions {
|
||||
timeoutMs?: number
|
||||
@@ -78,6 +78,11 @@ export async function checkServerHealth(
|
||||
const signal = opts?.signal ?? timeout?.signal
|
||||
const retryCount = opts?.retryCount ?? defaultRetryCount
|
||||
const retryDelayMs = opts?.retryDelayMs ?? defaultRetryDelayMs
|
||||
const headers = server.password
|
||||
? {
|
||||
Authorization: `Basic ${authTokenFromCredentials({ username: server.username, password: server.password })}`,
|
||||
}
|
||||
: undefined
|
||||
const next = (count: number, error: unknown) => {
|
||||
if (count >= retryCount || !retryable(error, signal)) return Promise.resolve({ healthy: false } as const)
|
||||
return wait(retryDelayMs * (count + 1), signal)
|
||||
@@ -88,18 +93,23 @@ export async function checkServerHealth(
|
||||
const current = await OpenCode.make({
|
||||
baseUrl: server.url,
|
||||
fetch,
|
||||
headers: server.password
|
||||
? {
|
||||
Authorization: `Basic ${authTokenFromCredentials({ username: server.username, password: server.password })}`,
|
||||
}
|
||||
: undefined,
|
||||
headers,
|
||||
})
|
||||
.health.get({ signal })
|
||||
.then((x) =>
|
||||
typeof x.healthy === "boolean"
|
||||
? { data: { healthy: x.healthy, version: x.version } }
|
||||
: { error: new Error("Invalid health response") },
|
||||
)
|
||||
.then(async (x) => {
|
||||
if (typeof x.healthy !== "boolean") return { error: new Error("Invalid health response") }
|
||||
if (x.healthy && typeof x.version !== "string") {
|
||||
const legacy = await fetch(new URL("/global/health", server.url), { headers, signal })
|
||||
.then((response) => response.json())
|
||||
.catch(() => undefined)
|
||||
const version =
|
||||
typeof legacy === "object" && legacy !== null && "version" in legacy && typeof legacy.version === "string"
|
||||
? legacy.version
|
||||
: "1"
|
||||
return { data: { healthy: false, version, incompatible: true } }
|
||||
}
|
||||
return { data: { healthy: x.healthy, version: x.version } }
|
||||
})
|
||||
.catch((error) => ({ error }))
|
||||
if ("data" in current && current.data) return current.data
|
||||
if (signal?.aborted) return { healthy: false }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
||||
import {
|
||||
children,
|
||||
@@ -119,13 +120,20 @@ export function ServerRow(props: ServerRowProps) {
|
||||
|
||||
export function ServerHealthIndicator(props: { health?: ServerHealth }) {
|
||||
return (
|
||||
<div
|
||||
classList={{
|
||||
"size-1.5 rounded-full shrink-0 my-[3.5px]": true,
|
||||
"bg-icon-success-base": props.health?.healthy === true,
|
||||
"bg-icon-critical-base": props.health?.healthy === false,
|
||||
"bg-border-weak-base": props.health === undefined,
|
||||
}}
|
||||
/>
|
||||
<Show
|
||||
when={props.health?.incompatible}
|
||||
fallback={
|
||||
<div
|
||||
classList={{
|
||||
"size-1.5 rounded-full shrink-0 my-[3.5px]": true,
|
||||
"bg-icon-success-base": props.health?.healthy === true,
|
||||
"bg-icon-critical-base": props.health?.healthy === false,
|
||||
"bg-border-weak-base": props.health === undefined,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Icon name="warning" size="small" class="shrink-0 text-icon-warning-base" />
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { Show } from "solid-js"
|
||||
import { useLanguage } from "@/runtime/i18n/language"
|
||||
import { useServer } from "@/runtime/server/current"
|
||||
import { serverName } from "@/runtime/server/registry"
|
||||
|
||||
export function IncompatibleServerPanel(props: { onClose?: () => void }) {
|
||||
const language = useLanguage()
|
||||
const server = useServer()
|
||||
|
||||
return (
|
||||
<div class="flex-1 min-h-0 overflow-hidden">
|
||||
<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-13-regular text-text-weak max-w-md">
|
||||
{language.t("session.error.incompatible.description", {
|
||||
server: serverName(server.conn),
|
||||
version: server.health?.version ?? "1",
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<Show when={props.onClose}>
|
||||
<Button variant="neutral" size="normal" icon="xmark-small" onClick={() => props.onClose?.()}>
|
||||
{language.t("session.error.notFound.closeTab")}
|
||||
</Button>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -2,51 +2,68 @@ import { Button } from "@opencode-ai/ui/button"
|
||||
import { createMemo, Show } from "solid-js"
|
||||
import { ErrorPage } from "@/shell/errors/error"
|
||||
import { useLanguage } from "@/runtime/i18n/language"
|
||||
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"
|
||||
|
||||
export function SessionErrorFallback(props: { error: unknown; sessionID?: string; serverKey?: ServerConnection.Key }) {
|
||||
const language = useLanguage()
|
||||
const activeServer = useServer()
|
||||
const server = useServers()
|
||||
const tabs = useTabs()
|
||||
const displayServer = createMemo(() => {
|
||||
const conn = server.list.find((item) => ServerConnection.key(item) === props.serverKey)
|
||||
return conn ? serverName(conn) : props.serverKey
|
||||
})
|
||||
|
||||
if (!isCurrentSessionNotFoundError(props.error, props.sessionID)) return <ErrorPage error={props.error} />
|
||||
const closeSession = () => {
|
||||
if (!props.sessionID) return
|
||||
tabs.removeSessionTab({ server: activeServer.key, sessionId: props.sessionID })
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="flex-1 min-h-0 overflow-hidden">
|
||||
<div class="h-full px-6 pb-42 -mt-4 flex flex-col items-center justify-center text-center gap-4">
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<div class="text-16-medium text-text max-w-md">{language.t("session.error.notFound")}</div>
|
||||
<div class="text-13-regular text-text-weak max-w-md">{language.t("session.error.notFound.description")}</div>
|
||||
</div>
|
||||
<Show when={props.sessionID}>
|
||||
{(sessionID) => (
|
||||
<div class="max-w-full flex flex-col items-center gap-1">
|
||||
<div class="max-w-full text-11-regular text-text-faint break-all">{displayServer()}</div>
|
||||
<code class="max-w-full rounded-[4px] px-1 py-0.5 font-mono text-xs font-medium leading-4 text-text-base break-all bg-[color-mix(in_oklch,var(--v2-text-text-base)_8%,transparent)]">
|
||||
{sessionID()}
|
||||
</code>
|
||||
<Show
|
||||
when={!activeServer.health?.incompatible}
|
||||
fallback={<IncompatibleServerPanel onClose={props.sessionID ? closeSession : undefined} />}
|
||||
>
|
||||
<Show
|
||||
when={isCurrentSessionNotFoundError(props.error, props.sessionID)}
|
||||
fallback={<ErrorPage error={props.error} />}
|
||||
>
|
||||
<div class="flex-1 min-h-0 overflow-hidden">
|
||||
<div class="h-full px-6 pb-42 -mt-4 flex flex-col items-center justify-center text-center gap-4">
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<div class="text-16-medium text-text max-w-md">{language.t("session.error.notFound")}</div>
|
||||
<div class="text-13-regular text-text-weak max-w-md">
|
||||
{language.t("session.error.notFound.description")}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
<Button
|
||||
variant="neutral"
|
||||
size="normal"
|
||||
icon="xmark-small"
|
||||
onClick={() => {
|
||||
if (!props.sessionID || !props.serverKey) return
|
||||
tabs.removeSessionTab({ server: props.serverKey, sessionId: props.sessionID })
|
||||
}}
|
||||
>
|
||||
{language.t("session.error.notFound.closeTab")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={props.sessionID}>
|
||||
{(sessionID) => (
|
||||
<div class="max-w-full flex flex-col items-center gap-1">
|
||||
<div class="max-w-full text-11-regular text-text-faint break-all">{displayServer()}</div>
|
||||
<code class="max-w-full rounded-[4px] px-1 py-0.5 font-mono text-xs font-medium leading-4 text-text-base break-all bg-[color-mix(in_oklch,var(--v2-text-text-base)_8%,transparent)]">
|
||||
{sessionID()}
|
||||
</code>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
<Button
|
||||
variant="neutral"
|
||||
size="normal"
|
||||
icon="xmark-small"
|
||||
onClick={() => {
|
||||
if (!props.sessionID || !props.serverKey) return
|
||||
tabs.removeSessionTab({ server: props.serverKey, sessionId: props.sessionID })
|
||||
}}
|
||||
>
|
||||
{language.t("session.error.notFound.closeTab")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,9 +12,11 @@ import { ServerConnection } from "@/runtime/server/registry"
|
||||
import { TerminalProvider } from "@/session/terminal/context"
|
||||
import { useSettingsCommand } from "@/settings/command"
|
||||
import { SessionUIProvider } from "@/shell/routes/session-ui-provider"
|
||||
import { useTabs } from "@/shell/tabs/tabs"
|
||||
import { requireServerKey } from "@/shell/routes/session"
|
||||
import { useSessionModel } from "./model"
|
||||
import { SessionPanelFrame, SessionRouteFrame } from "./session-frame"
|
||||
import { IncompatibleServerPanel } from "./incompatible-server-panel"
|
||||
import { SessionErrorFallback } from "./route-error"
|
||||
import { createSessionResolution } from "./session-resolution"
|
||||
import { SessionScreen } from "./screen"
|
||||
@@ -63,6 +65,7 @@ function SessionRouteErrorBoundary(
|
||||
function ResolvedTargetSessionRoute() {
|
||||
const params = useParams<{ id: string }>()
|
||||
const server = useServer()
|
||||
const tabs = useTabs()
|
||||
const data = useData()
|
||||
const current = createSessionResolution(
|
||||
() => params.id,
|
||||
@@ -72,14 +75,27 @@ function ResolvedTargetSessionRoute() {
|
||||
const directory = createMemo(() => current()?.location.directory)
|
||||
|
||||
return (
|
||||
<Show when={directory()}>
|
||||
{(value) => (
|
||||
<LocationProvider directory={value()}>
|
||||
<SessionUIProvider directory={value()} server={server.key}>
|
||||
<TargetSessionPage />
|
||||
</SessionUIProvider>
|
||||
</LocationProvider>
|
||||
)}
|
||||
<Show
|
||||
when={!server.health?.incompatible}
|
||||
fallback={
|
||||
<SessionRouteFrame padded>
|
||||
<SessionPanelFrame raised>
|
||||
<IncompatibleServerPanel
|
||||
onClose={() => tabs.removeSessionTab({ server: server.key, sessionId: params.id })}
|
||||
/>
|
||||
</SessionPanelFrame>
|
||||
</SessionRouteFrame>
|
||||
}
|
||||
>
|
||||
<Show when={directory()}>
|
||||
{(value) => (
|
||||
<LocationProvider directory={value()}>
|
||||
<SessionUIProvider directory={value()} server={server.key}>
|
||||
<TargetSessionPage />
|
||||
</SessionUIProvider>
|
||||
</LocationProvider>
|
||||
)}
|
||||
</Show>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user