diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index 25d2e3749a..42284ee2c4 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -47,7 +47,7 @@ import { ServerSyncProvider, useServerSync } from "@/context/server-sync" import { GlobalProvider, useGlobal } from "@/context/global" import { HighlightsProvider } from "@/context/highlights" import { LanguageProvider, type Locale, useLanguage } from "@/context/language" -import { LayoutProvider } from "@/context/layout" +import { currentRoute, LayoutProvider } from "@/context/layout" import { ModelsProvider } from "@/context/models" import { NotificationProvider } from "@/context/notification" import { PermissionProvider } from "@/context/permission" @@ -237,6 +237,44 @@ function UiI18nBridge(props: ParentProps) { return {props.children} } +function LayoutCompatibility(props: ParentProps) { + const global = useGlobal() + const location = useLocation() + const server = useServer() + const settings = useSettings() + const tabs = useTabs() + const connection = createMemo(() => { + const route = currentRoute(location.pathname, location.search) + if (route.type === "draft" && !tabs.ready()) return + const key = + route.type === "draft" + ? tabs.store.find((tab): tab is DraftTab => tab.type === "draft" && tab.draftID === route.draftID)?.server + : route.type === "home" + ? undefined + : route.server + if (!key) return server.current + return global.servers.list().find((item) => ServerConnection.key(item) === key) + }) + const protocol = createMemo(() => { + const value = connection() + if (!value) return + return global.ensureServerCtx(value).sdk.protocolKind() + }) + + createEffect(() => { + const value = protocol() + if (!value) return + settings.general.setNewLayoutRequired(value === "v2") + }) + const ready = createMemo(() => { + const value = protocol() + if (!value) return false + return settings.general.newLayoutRequired() === (value === "v2") + }) + + return {props.children} +} + declare global { interface Window { __OPENCODE__?: { @@ -558,15 +596,17 @@ export function AppInterface(props: { component={props.router ?? Router} root={(routerProps) => ( - - - - - {routerProps.children} - - - - + + + + + + {routerProps.children} + + + + + )} > diff --git a/packages/app/src/components/dialog-custom-provider.tsx b/packages/app/src/components/dialog-custom-provider.tsx index 9e04cd83ad..5db684a2da 100644 --- a/packages/app/src/components/dialog-custom-provider.tsx +++ b/packages/app/src/components/dialog-custom-provider.tsx @@ -131,6 +131,7 @@ export function CustomProviderForm(props: { autofocus?: boolean } = {}) { const saveMutation = useMutation(() => ({ mutationFn: async (result: NonNullable>) => { + if ((await serverSDK().protocol) !== "v1") throw new Error("Custom providers are unavailable on this server") const disabledProviders = serverSync().data.config.disabled_providers ?? [] const nextDisabled = disabledProviders.filter((id) => id !== result.providerID) diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx index 3beb97225a..3bd4cd10cc 100644 --- a/packages/app/src/components/settings-general.tsx +++ b/packages/app/src/components/settings-general.tsx @@ -13,7 +13,7 @@ import { useLanguage } from "@/context/language" import { usePermission } from "@/context/permission" import { usePlatform, type DisplayBackend } from "@/context/platform" import { useServerSync } from "@/context/server-sync" -import { useServerSDK } from "@/context/server-sdk" +import { useServerProtocol, useServerSDK } from "@/context/server-sdk" import { useUpdaterAction } from "./updater-action" import { monoDefault, @@ -125,10 +125,12 @@ export const SettingsGeneral: Component = () => { const serverSync = useServerSync() const serverSdk = useServerSDK() + const protocol = useServerProtocol() const [shells] = createResource( - () => - serverSdk() + () => (protocol() === "v1" ? serverSdk() : undefined), + (sdk) => + sdk .client.pty.shells() .then((res) => res.data ?? []) .catch(() => [] as ShellOption[]), @@ -322,27 +324,29 @@ export const SettingsGeneral: Component = () => { - - o.value === currentShell()) ?? autoOption} - value={(o) => o.id} - label={(o) => o.label} - onSelect={(option) => { - if (!option) return - if (option.value === currentShell()) return - serverSync().updateConfig({ shell: option.value }) - }} - variant="secondary" - size="small" - triggerVariant="settings" - triggerStyle={{ "min-width": "180px" }} - /> - + + + o.value === currentShell()) ?? autoOption} + value={(o) => o.id} + label={(o) => o.label} + onSelect={(option) => { + if (!option) return + if (option.value === currentShell()) return + serverSync().updateConfig({ shell: option.value }) + }} + variant="secondary" + size="small" + triggerVariant="settings" + triggerStyle={{ "min-width": "180px" }} + /> + + void }> = (props) => const dialog = useDialog() const language = useLanguage() const serverSDK = useServerSDK() + const protocol = useServerProtocol() const serverSync = useServerSync() const providers = useProviders() const providerConnect = useProviderConnectController({ onBack: props.onBack }) @@ -83,7 +84,8 @@ const SettingsProvidersContent: Component<{ onBack?: () => void }> = (props) => return language.t("settings.providers.tag.other") } - const canDisconnect = (item: ProviderItem) => source(item) !== "env" + const canDisconnect = (item: ProviderItem) => + source(item) !== "env" && (protocol() === "v1" || !isConfigCustom(item.id)) const note = (id: string) => PROVIDER_NOTES.find((item) => item.match(id))?.key @@ -96,6 +98,7 @@ const SettingsProvidersContent: Component<{ onBack?: () => void }> = (props) => } const disableProvider = async (providerID: string, name: string) => { + if (protocol() !== "v1") return const before = serverSync().data.config.disabled_providers ?? [] const next = before.includes(providerID) ? before : [...before, providerID] serverSync().set("config", "disabled_providers", next) @@ -218,31 +221,33 @@ const SettingsProvidersContent: Component<{ onBack?: () => void }> = (props) => )} - - - - - {language.t("provider.custom.title")} - {language.t("settings.providers.tag.custom")} - - - {language.t("settings.providers.custom.description")} - - - { - dialog.show(() => ) - }} + + - {language.t("common.connect")} - - + + + + {language.t("provider.custom.title")} + {language.t("settings.providers.tag.custom")} + + + {language.t("settings.providers.custom.description")} + + + { + dialog.show(() => ) + }} + > + {language.t("common.connect")} + + + (() => theme.ids().map((id) => ({ id, name: theme.name(id) }))) const [shells] = createResource( - () => - serverSdk() + () => (protocol() === "v1" ? serverSdk() : undefined), + (sdk) => + sdk .client.pty.shells() .then((res) => res.data ?? []) .catch(() => [] as ShellOption[]), @@ -281,26 +283,28 @@ export const SettingsGeneralV2: Component<{ - - o.value === currentShell()) ?? autoOption} - placement="bottom-end" - gutter={6} - value={(o) => o.id} - label={(o) => o.label} - onSelect={(option) => { - if (!option) return - if (option.value === currentShell()) return - serverSync().updateConfig({ shell: option.value }) - }} - /> - + + + o.value === currentShell()) ?? autoOption} + placement="bottom-end" + gutter={6} + value={(o) => o.id} + label={(o) => o.label} + onSelect={(option) => { + if (!option) return + if (option.value === currentShell()) return + serverSync().updateConfig({ shell: option.value }) + }} + /> + + - + + + diff --git a/packages/app/src/components/settings-v2/providers.tsx b/packages/app/src/components/settings-v2/providers.tsx index f945fa33c6..f4309b9b0d 100644 --- a/packages/app/src/components/settings-v2/providers.tsx +++ b/packages/app/src/components/settings-v2/providers.tsx @@ -6,7 +6,7 @@ import { showToast } from "@/utils/toast" import { popularProviders, useProviders } from "@/hooks/use-providers" import { createMemo, type Component, For, Show } from "solid-js" import { useLanguage } from "@/context/language" -import { useServerSDK } from "@/context/server-sdk" +import { useServerProtocol, useServerSDK } from "@/context/server-sdk" import { useServerSync } from "@/context/server-sync" import { DialogConnectProvider, useProviderConnectController } from "../dialog-connect-provider" import { DialogCustomProvider } from "../dialog-custom-provider" @@ -33,6 +33,7 @@ export const SettingsProvidersV2: Component<{ onBack?: () => void }> = (props) = const dialog = useDialog() const language = useLanguage() const serverSdk = useServerSDK() + const protocol = useServerProtocol() const serverSync = useServerSync() const providers = useProviders() const providerConnect = useProviderConnectController({ onBack: props.onBack }) @@ -77,7 +78,8 @@ export const SettingsProvidersV2: Component<{ onBack?: () => void }> = (props) = return language.t("settings.providers.tag.other") } - const canDisconnect = (item: ProviderItem) => source(item) !== "env" + const canDisconnect = (item: ProviderItem) => + source(item) !== "env" && (protocol() === "v1" || !isConfigCustom(item.id)) const note = (id: string) => PROVIDER_NOTES.find((item) => item.match(id))?.key @@ -90,6 +92,7 @@ export const SettingsProvidersV2: Component<{ onBack?: () => void }> = (props) = } const disableProvider = async (providerID: string, name: string) => { + if (protocol() !== "v1") return const before = serverSync().data.config.disabled_providers ?? [] const next = before.includes(providerID) ? before : [...before, providerID] serverSync().set("config", "disabled_providers", next) @@ -218,33 +221,35 @@ export const SettingsProvidersV2: Component<{ onBack?: () => void }> = (props) = )} - - - - - - {language.t("provider.custom.title")} - {language.t("settings.providers.tag.custom")} + + + + + + + {language.t("provider.custom.title")} + {language.t("settings.providers.tag.custom")} + + {language.t("settings.providers.custom.description")} - {language.t("settings.providers.custom.description")} + { + dialog.show(() => ) + }} + > + {language.t("common.connect")} + - { - dialog.show(() => ) - }} - > - {language.t("common.connect")} - - + connect()}> diff --git a/packages/app/src/components/status-popover-body.tsx b/packages/app/src/components/status-popover-body.tsx index 68a3f6b226..71f931372d 100644 --- a/packages/app/src/components/status-popover-body.tsx +++ b/packages/app/src/components/status-popover-body.tsx @@ -16,6 +16,7 @@ import { type ServerHealth } from "@/utils/server-health" import { useGlobal } from "@/context/global" import { useSettings } from "@/context/settings" import { useMcpToggle } from "@/context/mcp" +import { useServerProtocol } from "@/context/server-sdk" const pluginEmptyMessage = (value: string, file: string): JSXElement => { const parts = value.split(file) @@ -257,6 +258,7 @@ export function StatusPopoverBody(props: { shown: Accessor }) { const language = useLanguage() const navigate = useNavigate() const settings = useSettings() + const protocol = useServerProtocol() const fail = (err: unknown) => { showToast({ @@ -315,10 +317,12 @@ export function StatusPopoverBody(props: { shown: Accessor }) { {lspCount() > 0 ? `${lspCount()} ` : ""} {language.t("status.popover.tab.lsp")} - - {pluginCount() > 0 ? `${pluginCount()} ` : ""} - {language.t("status.popover.tab.plugins")} - + + + {pluginCount() > 0 ? `${pluginCount()} ` : ""} + {language.t("status.popover.tab.plugins")} + + {!settings.general.newLayoutDesigns() && ( @@ -478,25 +482,27 @@ export function StatusPopoverBody(props: { shown: Accessor }) { - - - - 0} - fallback={{pluginEmpty()}} - > - - {(plugin) => ( - - - {plugin} - - )} - - + + + + + 0} + fallback={{pluginEmpty()}} + > + + {(plugin) => ( + + + {plugin} + + )} + + + - - + + ) diff --git a/packages/app/src/context/layout.tsx b/packages/app/src/context/layout.tsx index 7ac248dc1d..60c05e402e 100644 --- a/packages/app/src/context/layout.tsx +++ b/packages/app/src/context/layout.tsx @@ -126,7 +126,7 @@ const normalizeStoredSessionTabs = (key: string, tabs: SessionTabs) => { } } -const currentRoute = (pathname: string, search: string): LayoutRoute => { +export const currentRoute = (pathname: string, search: string): LayoutRoute => { const parts = pathname.split("/").filter(Boolean) if (parts.length === 0) return { type: "home" } diff --git a/packages/app/src/context/permission.tsx b/packages/app/src/context/permission.tsx index 496d1ab4a2..4f4a285d3b 100644 --- a/packages/app/src/context/permission.tsx +++ b/packages/app/src/context/permission.tsx @@ -211,6 +211,7 @@ function createServerPermissionState(input: { sdk: ServerSDK; sync: ServerSync } ) function enableConfiguredDirectory(directory: string) { + if (input.sdk.protocolKind() !== "v1") return if (meta.disposed || !ready()) return const [childStore] = input.sync.child(directory) if (childStore.config.permission !== "allow") return diff --git a/packages/app/src/context/server-sdk.tsx b/packages/app/src/context/server-sdk.tsx index 62c5857794..4c879603bd 100644 --- a/packages/app/src/context/server-sdk.tsx +++ b/packages/app/src/context/server-sdk.tsx @@ -3,7 +3,7 @@ import type { Event } from "@opencode-ai/sdk/v2/client" import { createSimpleContext } from "@opencode-ai/ui/context" import { createGlobalEmitter } from "@solid-primitives/event-bus" import { makeEventListener } from "@solid-primitives/event-listener" -import { type Accessor, batch, createMemo, onCleanup, onMount } from "solid-js" +import { type Accessor, batch, createMemo, createResource, onCleanup, onMount } from "solid-js" import { createApiForServer, createSdkForServer, type ServerApi } from "@/utils/server" import { useLanguage } from "./language" import { usePlatform } from "./platform" @@ -169,6 +169,7 @@ type ServerSDKBase = { server: ServerConnection.Any scope: ServerScope protocol: Promise + protocolKind: Accessor url: string client: ReturnType api: CompatibleApi @@ -205,6 +206,10 @@ function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerS server: server.http, }) const protocol = detectServerProtocol(server.http, platform.fetch ?? globalThis.fetch) + const [protocolKind] = createResource( + () => protocol, + (value) => value, + ) const emitter = createGlobalEmitter<{ [key: string]: ServerEvent }>() @@ -347,6 +352,7 @@ function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerS server, scope, protocol, + protocolKind, url: server.http.url, client: sdk, api, @@ -394,6 +400,11 @@ export const { use: useServerSDK, provider: ServerSDKProvider } = createSimpleCo }, }) +export function useServerProtocol() { + const serverSDK = useServerSDK() + return createMemo(() => serverSDK().protocolKind()) +} + type SDKEventMap = { [key in Event["type"]]: Extract } diff --git a/packages/app/src/context/settings.tsx b/packages/app/src/context/settings.tsx index c2b5680418..9b44273d0f 100644 --- a/packages/app/src/context/settings.tsx +++ b/packages/app/src/context/settings.tsx @@ -230,6 +230,7 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont migrationApplied: false, previous: undefined as string | undefined, }) + const [newLayoutRequired, setNewLayoutRequired] = createSignal(false) const showFileTree = withFallback(() => store.general?.showFileTree, defaultSettings.general.showFileTree) const showSearch = withFallback(() => store.general?.showSearch, defaultSettings.general.showSearch) const showStatus = withFallback(() => store.general?.showStatus, defaultSettings.general.showStatus) @@ -250,7 +251,7 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont const layoutTransition = createMemo(() => layoutTransitionState(!!sunset, layoutTransitionEligible(), oldInterfaceRetired(), newInterfaceNoticeDismissed()), ) - const newLayoutDesigns = createMemo(() => { + const preferredNewLayout = createMemo(() => { if (layoutUpgrade()) return true if (!ready() && !oldInterfaceRetired()) return legacyNewLayoutDesignsDefault if (!layoutTransitionClassified()) { @@ -266,6 +267,7 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont layoutTransitionEligible() ? legacyNewLayoutDesignsDefault : newLayoutDesignsDefault, ) }) + const newLayoutDesigns = createMemo(() => newLayoutRequired() || preferredNewLayout()) const visible = (preference: () => boolean) => createMemo(() => !newLayoutDesigns() || preference()) if (sunset && !oldInterfaceRetired()) { @@ -404,9 +406,12 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont setStore("general", "mobileTitlebarPosition", value) }, newLayoutDesigns, + newLayoutRequired, + setNewLayoutRequired, setNewLayoutDesigns(value: boolean) { + if (newLayoutRequired() && !value) return const next = oldInterfaceRetired() ? true : value - if (newLayoutDesigns() === next) return + if (preferredNewLayout() === next) return setStore("general", "newLayoutDesigns", next) if (typeof window !== "undefined") setTimeout(() => window.location.reload()) }, diff --git a/packages/app/src/pages/session/timeline/message-timeline.tsx b/packages/app/src/pages/session/timeline/message-timeline.tsx index 2959e9f8a9..147ded1954 100644 --- a/packages/app/src/pages/session/timeline/message-timeline.tsx +++ b/packages/app/src/pages/session/timeline/message-timeline.tsx @@ -62,7 +62,7 @@ import { SessionContextUsage } from "@/components/session-context-usage" import { useDialog } from "@opencode-ai/ui/context/dialog" import { useLanguage } from "@/context/language" import { useSessionKey } from "@/pages/session/session-layout" -import { useServerSDK } from "@/context/server-sdk" +import { useServerProtocol, useServerSDK } from "@/context/server-sdk" import { usePlatform } from "@/context/platform" import { useSettings } from "@/context/settings" import { useTabs } from "@/context/tabs" @@ -291,7 +291,8 @@ export function MessageTimeline(props: { const titleValue = createMemo(() => info()?.title) const titleLabel = createMemo(() => sessionTitle(titleValue())) const shareUrl = createMemo(() => info()?.share?.url) - const shareEnabled = createMemo(() => sync().data.config.share !== "disabled") + const protocol = useServerProtocol() + const shareEnabled = createMemo(() => protocol() === "v1" && sync().data.config.share !== "disabled") const parentID = createMemo(() => info()?.parentID) const parent = createMemo(() => { const id = parentID() diff --git a/packages/app/src/pages/session/use-session-commands.tsx b/packages/app/src/pages/session/use-session-commands.tsx index 275e6ec4bc..e2ac73a95d 100644 --- a/packages/app/src/pages/session/use-session-commands.tsx +++ b/packages/app/src/pages/session/use-session-commands.tsx @@ -19,6 +19,7 @@ import { extractPromptFromParts } from "@/utils/prompt" import { UserMessage } from "@opencode-ai/sdk/v2" import { useSessionLayout } from "@/pages/session/session-layout" import { createSessionOwnership } from "./session-ownership" +import { useServerProtocol } from "@/context/server-sdk" export type SessionCommandContext = { navigateMessageByOffset: (offset: number) => void @@ -45,6 +46,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => { const prompt = usePrompt() const sdk = useSDK() const settings = useSettings() + const protocol = useServerProtocol() const sync = useSync() const terminal = useTerminal() const layout = useLayout() @@ -184,6 +186,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => { } const share = async () => { + if (protocol() !== "v1") return const sessionID = params.id if (!sessionID) return @@ -210,6 +213,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => { } const unshare = async () => { + if (protocol() !== "v1") return const sessionID = params.id if (!sessionID) return @@ -390,6 +394,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => { } const shareCmds = () => { + if (protocol() !== "v1") return [] if (sync().data.config.share === "disabled") return [] return [ sessionCommand({
{language.t("settings.providers.custom.description")}