Skip to content

Commit c2565db

Browse files
authored
Merge branch 'dev' into feat/fff-search-tools
2 parents 9b66590 + 4814ab3 commit c2565db

48 files changed

Lines changed: 2835 additions & 212 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/app/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"exports": {
77
".": "./src/index.ts",
88
"./desktop-menu": "./src/desktop-menu.ts",
9+
"./wsl/types": "./src/wsl/types.ts",
910
"./vite": "./vite.js",
1011
"./index.css": "./src/index.css"
1112
},

packages/app/src/app.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import { ServerConnection, ServerProvider, serverName, useServer } from "@/conte
4444
import { SettingsProvider, useSettings } from "@/context/settings"
4545
import { TerminalProvider } from "@/context/terminal"
4646
import { TabsProvider } from "@/context/tabs"
47+
import { WslServersProvider } from "@/wsl/context"
4748
import DirectoryLayout from "@/pages/directory-layout"
4849
import Layout from "@/pages/layout"
4950
import { ErrorPage } from "./pages/error"
@@ -71,7 +72,6 @@ declare global {
7172
__OPENCODE__?: {
7273
updaterEnabled?: boolean
7374
deepLinks?: string[]
74-
wsl?: boolean
7575
}
7676
api?: {
7777
setTitlebar?: (theme: { mode: "light" | "dark" }) => Promise<void>
@@ -171,11 +171,13 @@ export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) {
171171
}}
172172
>
173173
<QueryProvider>
174-
<DialogProvider>
175-
<MarkedProvider>
176-
<FileComponentProvider component={File}>{props.children}</FileComponentProvider>
177-
</MarkedProvider>
178-
</DialogProvider>
174+
<WslServersProvider>
175+
<DialogProvider>
176+
<MarkedProvider>
177+
<FileComponentProvider component={File}>{props.children}</FileComponentProvider>
178+
</MarkedProvider>
179+
</DialogProvider>
180+
</WslServersProvider>
179181
</QueryProvider>
180182
</ErrorBoundary>
181183
</UiI18nBridge>

packages/app/src/components/dialog-select-file.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,11 @@ function createSessionEntries(props: {
261261
return { sessions }
262262
}
263263

264-
export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFile?: (path: string) => void }) {
264+
export function DialogSelectFile(props: {
265+
mode?: DialogSelectFileMode
266+
onOpenFile?: (path: string) => void
267+
onSelectFile?: (path: string) => void
268+
}) {
265269
const command = useCommand()
266270
const language = useLanguage()
267271
const layout = useLayout()
@@ -375,6 +379,10 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
375379
}
376380

377381
if (!item.path) return
382+
if (props.onSelectFile) {
383+
props.onSelectFile(item.path)
384+
return
385+
}
378386
open(item.path)
379387
}
380388

packages/app/src/components/prompt-input.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import { usePermission } from "@/context/permission"
5252
import { useLanguage } from "@/context/language"
5353
import { usePlatform } from "@/context/platform"
5454
import { useSettings } from "@/context/settings"
55+
import { serverAttachmentFile } from "./prompt-input/server-attachment"
5556
import { useSessionLayout } from "@/pages/session/session-layout"
5657
import { createSessionTabs } from "@/pages/session/helpers"
5758
import { createTextFragment, getCursorPosition, setCursorPosition, setRangeEdge } from "./prompt-input/editor-dom"
@@ -465,7 +466,25 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
465466

466467
const escBlur = () => platform.platform === "desktop" && platform.os === "macos"
467468

468-
const pick = () => fileInputRef?.click()
469+
const pick = () => {
470+
if (server.isLocal()) {
471+
fileInputRef?.click()
472+
return
473+
}
474+
void import("@/components/dialog-select-file").then((module) =>
475+
dialog.show(() => (
476+
<module.DialogSelectFile
477+
mode="files"
478+
onSelectFile={(path) => {
479+
void sdk.client.v2.fs
480+
.read({ path })
481+
.then((response) => response.data?.data)
482+
.then((data) => data && addAttachments([serverAttachmentFile(path, data)]))
483+
}}
484+
/>
485+
)),
486+
)
487+
}
469488

470489
const setMode = (mode: "normal" | "shell") => {
471490
setStore("mode", mode)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { describe, expect, test } from "bun:test"
2+
import { serverAttachmentFile } from "./server-attachment"
3+
4+
describe("serverAttachmentFile", () => {
5+
test("creates a file from server text content", async () => {
6+
const file = serverAttachmentFile("docs/readme.txt", { type: "text", content: "hello", mime: "text/plain" })
7+
8+
expect(file.name).toBe("readme.txt")
9+
expect(file.type).toBe("text/plain")
10+
expect(await file.text()).toBe("hello")
11+
})
12+
13+
test("creates a file from server base64 content", async () => {
14+
const file = serverAttachmentFile("images/pixel.png", {
15+
type: "binary",
16+
content: "aGVsbG8=",
17+
encoding: "base64",
18+
mime: "image/png",
19+
})
20+
21+
expect(file.name).toBe("pixel.png")
22+
expect(file.type).toBe("image/png")
23+
expect(await file.text()).toBe("hello")
24+
})
25+
})
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { getFilename } from "@opencode-ai/core/util/path"
2+
import type { FileSystemBinaryContent, FileSystemTextContent } from "@opencode-ai/sdk/v2"
3+
4+
export function serverAttachmentFile(path: string, data: FileSystemTextContent | FileSystemBinaryContent) {
5+
const content =
6+
data.type === "text" ? data.content : Uint8Array.from(atob(data.content), (char) => char.charCodeAt(0))
7+
return new File([content], getFilename(path), { type: data.mime })
8+
}

packages/app/src/components/settings-v2/servers.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,22 @@ import { ServerConnection, serverName } from "@/context/server"
1414
import { useServerManagementController } from "../dialog-select-server"
1515
import { DialogServerV2 } from "./dialog-server-v2"
1616
import { SettingsListV2 } from "./parts/list"
17+
import { isWslServer, useFilteredWslServers, WslAddServerButton, WslServerSettings } from "@/wsl/settings"
1718
import "./settings-v2.css"
1819

1920
export const SettingsServersV2: Component = () => {
2021
const dialog = useDialog()
2122
const language = useLanguage()
2223
const controller = useServerManagementController()
2324
const [store, setStore] = createStore({ filter: "" })
25+
const wslServers = useFilteredWslServers(() => store.filter)
2426

25-
const showSearch = createMemo(() => controller.sortedItems().length > 1)
27+
const showSearch = createMemo(
28+
() => controller.sortedItems().filter((item) => !isWslServer(item)).length + wslServers().length > 1,
29+
)
2630

2731
const filtered = createMemo(() => {
28-
const items = controller.sortedItems()
32+
const items = controller.sortedItems().filter((item) => !isWslServer(item))
2933
const query = store.filter.trim()
3034
if (!query) return items
3135
return fuzzysort
@@ -54,6 +58,7 @@ export const SettingsServersV2: Component = () => {
5458
<ButtonV2 variant="ghost-muted" icon="plus" onClick={openAdd}>
5559
{language.t("dialog.server.add.button")}
5660
</ButtonV2>
61+
<WslAddServerButton />
5762
</div>
5863
<Show when={showSearch()}>
5964
<div class="settings-v2-tab-search">
@@ -85,7 +90,7 @@ export const SettingsServersV2: Component = () => {
8590

8691
<div class="settings-v2-tab-body settings-v2-servers">
8792
<Show
88-
when={filtered().length > 0}
93+
when={filtered().length > 0 || wslServers().length > 0}
8994
fallback={
9095
<div class="settings-v2-servers-status">
9196
<span>{store.filter ? language.t("palette.empty") : language.t("dialog.server.empty")}</span>
@@ -96,6 +101,7 @@ export const SettingsServersV2: Component = () => {
96101
}
97102
>
98103
<SettingsListV2>
104+
<WslServerSettings controller={controller} servers={wslServers} />
99105
<For each={filtered()}>
100106
{(item) => {
101107
const key = ServerConnection.key(item)

packages/app/src/components/status-popover-body.tsx

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,14 @@ import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row
1212
import { useLanguage } from "@/context/language"
1313
import { usePlatform } from "@/context/platform"
1414
import { useSDK } from "@/context/sdk"
15-
import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server"
15+
import { ServerConnection, useServer } from "@/context/server"
1616
import { useSync } from "@/context/sync"
1717
import { type ServerHealth } from "@/utils/server-health"
1818
import { useQueryOptions } from "@/context/server-sync"
1919
import { pathKey } from "@/utils/path-key"
2020
import { useGlobal } from "@/context/global"
2121
import { useSettings } from "@/context/settings"
2222

23-
const pollMs = 10_000
24-
2523
const pluginEmptyMessage = (value: string, file: string): JSXElement => {
2624
const parts = value.split(file)
2725
if (parts.length === 1) return value
@@ -60,7 +58,7 @@ const useDefaultServerKey = (
6058
get: (() => string | Promise<string | null | undefined> | null | undefined) | undefined,
6159
) => {
6260
const [state, setState] = createStore({
63-
url: undefined as string | undefined,
61+
key: undefined as ServerConnection.Key | undefined,
6462
tick: 0,
6563
})
6664

@@ -69,7 +67,7 @@ const useDefaultServerKey = (
6967
let dead = false
7068
const result = get?.()
7169
if (!result) {
72-
setState("url", undefined)
70+
setState("key", undefined)
7371
onCleanup(() => {
7472
dead = true
7573
})
@@ -79,25 +77,23 @@ const useDefaultServerKey = (
7977
if (result instanceof Promise) {
8078
void result.then((next) => {
8179
if (dead) return
82-
setState("url", next ? normalizeServerUrl(next) : undefined)
80+
setState("key", next ?? undefined)
8381
})
8482
onCleanup(() => {
8583
dead = true
8684
})
8785
return
8886
}
8987

90-
setState("url", normalizeServerUrl(result))
88+
setState("key", ServerConnection.Key.make(result))
9189
onCleanup(() => {
9290
dead = true
9391
})
9492
})
9593

9694
return {
9795
key: () => {
98-
const u = state.url
99-
if (!u) return
100-
return ServerConnection.key({ type: "http", http: { url: u } })
96+
return state.key
10197
},
10298
refresh: () => setState("tick", (value) => value + 1),
10399
}
@@ -160,7 +156,6 @@ export function StatusPopoverServerBody() {
160156
const dialog = useDialog()
161157
const language = useLanguage()
162158
const navigate = useNavigate()
163-
164159
let dialogRun = 0
165160
let dialogDead = false
166161
onCleanup(() => {

packages/app/src/context/platform.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { AsyncStorage, SyncStorage } from "@solid-primitives/storage"
33
import type { Accessor } from "solid-js"
44
import type { DesktopMenuAction } from "../desktop-menu"
55
import { ServerConnection } from "./server"
6+
import type { WslServersPlatform } from "../wsl/types"
67

78
type PickerPaths = string | string[] | null
89
type OpenDirectoryPickerOptions = { title?: string; multiple?: boolean }
@@ -75,11 +76,8 @@ export type Platform = {
7576
/** Set the default server URL to use on app startup (platform-specific) */
7677
setDefaultServer?(url: ServerConnection.Key | null): Promise<void> | void
7778

78-
/** Get the configured WSL integration (desktop only) */
79-
getWslEnabled?(): Promise<boolean>
80-
81-
/** Set the configured WSL integration (desktop only) */
82-
setWslEnabled?(config: boolean): Promise<void> | void
79+
/** Manage WSL sidecar servers (Electron on Windows only) */
80+
wslServers?: WslServersPlatform
8381

8482
/** Get the preferred display backend (desktop only) */
8583
getDisplayBackend?(): Promise<DisplayBackend | null> | DisplayBackend | null

packages/app/src/context/server.test.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { describe, expect, test } from "bun:test"
22
import { createRoot, createSignal } from "solid-js"
33
import { createStore } from "solid-js/store"
4-
import { createServerProjects, migrateCanonicalLocalServerState, resolveServerList, ServerConnection } from "./server"
4+
import {
5+
createServerProjects,
6+
migrateCanonicalLocalServerState,
7+
nextServerAfterRemoval,
8+
resolveServerList,
9+
ServerConnection,
10+
} from "./server"
511
import { ServerScope } from "@/utils/server-scope"
612

713
describe("resolveServerList", () => {
@@ -55,6 +61,40 @@ describe("resolveServerList", () => {
5561
})
5662
})
5763

64+
test("treats WSL sidecars as remote server connections", () => {
65+
expect(
66+
ServerConnection.local({
67+
type: "sidecar",
68+
variant: "wsl",
69+
distro: "Debian",
70+
http: { url: "http://127.0.0.1:4097" },
71+
}),
72+
).toBe(false)
73+
expect(ServerConnection.local({ type: "sidecar", variant: "base", http: { url: "http://127.0.0.1:4096" } })).toBe(
74+
true,
75+
)
76+
expect(ServerConnection.local({ type: "http", http: { url: "http://localhost:4096" } })).toBe(true)
77+
expect(ServerConnection.local({ type: "http", http: { url: "https://server.example.test" } })).toBe(false)
78+
})
79+
80+
test("active server removal falls back across built-in and persisted servers", () => {
81+
const local = { type: "sidecar", variant: "base", http: { url: "http://127.0.0.1:4096" } } as const
82+
const debian = {
83+
type: "sidecar",
84+
variant: "wsl",
85+
distro: "Debian",
86+
http: { url: "http://127.0.0.1:4097" },
87+
} as const
88+
89+
expect(
90+
nextServerAfterRemoval(
91+
[local, debian],
92+
ServerConnection.Key.make("wsl:Debian"),
93+
ServerConnection.Key.make("sidecar"),
94+
),
95+
).toBe(ServerConnection.Key.make("sidecar"))
96+
})
97+
5898
describe("createServerProjects", () => {
5999
test("keeps active and explicit server buckets in one reactive store", () => {
60100
createRoot((dispose) => {

0 commit comments

Comments
 (0)