feat: desktop v2 everything WSL (#23407)

This commit is contained in:
Luke Parker
2026-06-06 14:38:06 +10:00
committed by GitHub
parent 47e40c255f
commit b2632225c9
37 changed files with 2620 additions and 202 deletions
@@ -261,7 +261,11 @@ function createSessionEntries(props: {
return { sessions }
}
export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFile?: (path: string) => void }) {
export function DialogSelectFile(props: {
mode?: DialogSelectFileMode
onOpenFile?: (path: string) => void
onSelectFile?: (path: string) => void
}) {
const command = useCommand()
const language = useLanguage()
const layout = useLayout()
@@ -375,6 +379,10 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
}
if (!item.path) return
if (props.onSelectFile) {
props.onSelectFile(item.path)
return
}
open(item.path)
}
+20 -1
View File
@@ -52,6 +52,7 @@ import { usePermission } from "@/context/permission"
import { useLanguage } from "@/context/language"
import { usePlatform } from "@/context/platform"
import { useSettings } from "@/context/settings"
import { serverAttachmentFile } from "./prompt-input/server-attachment"
import { useSessionLayout } from "@/pages/session/session-layout"
import { createSessionTabs } from "@/pages/session/helpers"
import { createTextFragment, getCursorPosition, setCursorPosition, setRangeEdge } from "./prompt-input/editor-dom"
@@ -465,7 +466,25 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const escBlur = () => platform.platform === "desktop" && platform.os === "macos"
const pick = () => fileInputRef?.click()
const pick = () => {
if (server.isLocal()) {
fileInputRef?.click()
return
}
void import("@/components/dialog-select-file").then((module) =>
dialog.show(() => (
<module.DialogSelectFile
mode="files"
onSelectFile={(path) => {
void sdk.client.v2.fs
.read({ path })
.then((response) => response.data?.data)
.then((data) => data && addAttachments([serverAttachmentFile(path, data)]))
}}
/>
)),
)
}
const setMode = (mode: "normal" | "shell") => {
setStore("mode", mode)
@@ -0,0 +1,25 @@
import { describe, expect, test } from "bun:test"
import { serverAttachmentFile } from "./server-attachment"
describe("serverAttachmentFile", () => {
test("creates a file from server text content", async () => {
const file = serverAttachmentFile("docs/readme.txt", { type: "text", content: "hello", mime: "text/plain" })
expect(file.name).toBe("readme.txt")
expect(file.type).toBe("text/plain")
expect(await file.text()).toBe("hello")
})
test("creates a file from server base64 content", async () => {
const file = serverAttachmentFile("images/pixel.png", {
type: "binary",
content: "aGVsbG8=",
encoding: "base64",
mime: "image/png",
})
expect(file.name).toBe("pixel.png")
expect(file.type).toBe("image/png")
expect(await file.text()).toBe("hello")
})
})
@@ -0,0 +1,7 @@
import { getFilename } from "@opencode-ai/core/util/path"
import type { FileSystemBinaryContent, FileSystemTextContent } from "@opencode-ai/sdk/v2"
export function serverAttachmentFile(path: string, data: FileSystemTextContent | FileSystemBinaryContent) {
const content = data.type === "text" ? data.content : Uint8Array.from(atob(data.content), (char) => char.charCodeAt(0))
return new File([content], getFilename(path), { type: data.mime })
}
@@ -14,6 +14,7 @@ import { ServerConnection, serverName } from "@/context/server"
import { useServerManagementController } from "../dialog-select-server"
import { DialogServerV2 } from "./dialog-server-v2"
import { SettingsListV2 } from "./parts/list"
import { isWslServer, useFilteredWslServers, WslAddServerButton, WslServerSettings } from "@/wsl/settings"
import "./settings-v2.css"
export const SettingsServersV2: Component = () => {
@@ -21,11 +22,12 @@ export const SettingsServersV2: Component = () => {
const language = useLanguage()
const controller = useServerManagementController()
const [store, setStore] = createStore({ filter: "" })
const wslServers = useFilteredWslServers(() => store.filter)
const showSearch = createMemo(() => controller.sortedItems().length > 1)
const showSearch = createMemo(() => controller.sortedItems().filter((item) => !isWslServer(item)).length + wslServers().length > 1)
const filtered = createMemo(() => {
const items = controller.sortedItems()
const items = controller.sortedItems().filter((item) => !isWslServer(item))
const query = store.filter.trim()
if (!query) return items
return fuzzysort
@@ -54,6 +56,7 @@ export const SettingsServersV2: Component = () => {
<ButtonV2 variant="ghost-muted" icon="plus" onClick={openAdd}>
{language.t("dialog.server.add.button")}
</ButtonV2>
<WslAddServerButton />
</div>
<Show when={showSearch()}>
<div class="settings-v2-tab-search">
@@ -85,7 +88,7 @@ export const SettingsServersV2: Component = () => {
<div class="settings-v2-tab-body settings-v2-servers">
<Show
when={filtered().length > 0}
when={filtered().length > 0 || wslServers().length > 0}
fallback={
<div class="settings-v2-servers-status">
<span>{store.filter ? language.t("palette.empty") : language.t("dialog.server.empty")}</span>
@@ -96,6 +99,7 @@ export const SettingsServersV2: Component = () => {
}
>
<SettingsListV2>
<WslServerSettings controller={controller} servers={wslServers} />
<For each={filtered()}>
{(item) => {
const key = ServerConnection.key(item)
@@ -12,7 +12,7 @@ import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row
import { useLanguage } from "@/context/language"
import { usePlatform } from "@/context/platform"
import { useSDK } from "@/context/sdk"
import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server"
import { ServerConnection, useServer } from "@/context/server"
import { useSync } from "@/context/sync"
import { type ServerHealth } from "@/utils/server-health"
import { useQueryOptions } from "@/context/server-sync"
@@ -20,8 +20,6 @@ import { pathKey } from "@/utils/path-key"
import { useGlobal } from "@/context/global"
import { useSettings } from "@/context/settings"
const pollMs = 10_000
const pluginEmptyMessage = (value: string, file: string): JSXElement => {
const parts = value.split(file)
if (parts.length === 1) return value
@@ -60,7 +58,7 @@ const useDefaultServerKey = (
get: (() => string | Promise<string | null | undefined> | null | undefined) | undefined,
) => {
const [state, setState] = createStore({
url: undefined as string | undefined,
key: undefined as ServerConnection.Key | undefined,
tick: 0,
})
@@ -69,7 +67,7 @@ const useDefaultServerKey = (
let dead = false
const result = get?.()
if (!result) {
setState("url", undefined)
setState("key", undefined)
onCleanup(() => {
dead = true
})
@@ -79,7 +77,7 @@ const useDefaultServerKey = (
if (result instanceof Promise) {
void result.then((next) => {
if (dead) return
setState("url", next ? normalizeServerUrl(next) : undefined)
setState("key", next ?? undefined)
})
onCleanup(() => {
dead = true
@@ -87,7 +85,7 @@ const useDefaultServerKey = (
return
}
setState("url", normalizeServerUrl(result))
setState("key", ServerConnection.Key.make(result))
onCleanup(() => {
dead = true
})
@@ -95,9 +93,7 @@ const useDefaultServerKey = (
return {
key: () => {
const u = state.url
if (!u) return
return ServerConnection.key({ type: "http", http: { url: u } })
return state.key
},
refresh: () => setState("tick", (value) => value + 1),
}
@@ -160,7 +156,6 @@ export function StatusPopoverServerBody() {
const dialog = useDialog()
const language = useLanguage()
const navigate = useNavigate()
let dialogRun = 0
let dialogDead = false
onCleanup(() => {