add remote server launcher flow

This commit is contained in:
Shantur Rathore
2026-04-02 16:08:54 +01:00
parent 893d5f9296
commit 69d9e95bee
19 changed files with 1150 additions and 111 deletions

View File

@@ -1,6 +1,7 @@
import { createContext, createMemo, createSignal, onMount, useContext } from "solid-js"
import type { Accessor, ParentComponent } from "solid-js"
import { storage, type OwnerBucket } from "../lib/storage"
import type { RemoteServerProfile } from "../../../server/src/api-types"
import {
ensureInstanceConfigLoaded,
getInstanceConfig,
@@ -104,6 +105,7 @@ interface ServerConfigBucket {
interface UiStateBucket {
recentFolders?: RecentFolder[]
opencodeBinaries?: OpenCodeBinary[]
remoteServers?: RemoteServerProfile[]
models?: {
recents?: ModelPreference[]
favorites?: ModelPreference[]
@@ -114,6 +116,7 @@ interface UiStateBucket {
interface NormalizedUiState {
recentFolders: RecentFolder[]
opencodeBinaries: OpenCodeBinary[]
remoteServers: RemoteServerProfile[]
models: {
recents: ModelPreference[]
favorites: ModelPreference[]
@@ -252,6 +255,29 @@ function normalizeUiState(input?: UiStateBucket | null): NormalizedUiState {
const label = typeof (b as any).label === "string" ? (b as any).label : undefined
return { path: p, version, label, lastUsed }
}),
remoteServers: cloneArray<RemoteServerProfile>(source.remoteServers, (server) => {
if (!server || typeof server !== "object") return null
const id = typeof (server as any).id === "string" ? (server as any).id.trim() : ""
const name = typeof (server as any).name === "string" ? (server as any).name.trim() : ""
const baseUrl = typeof (server as any).baseUrl === "string" ? (server as any).baseUrl.trim() : ""
if (!id || !name || !baseUrl) return null
const createdAt = typeof (server as any).createdAt === "string" ? (server as any).createdAt : new Date().toISOString()
const updatedAt = typeof (server as any).updatedAt === "string" ? (server as any).updatedAt : createdAt
const lastConnectedAt = typeof (server as any).lastConnectedAt === "string" ? (server as any).lastConnectedAt : undefined
return {
id,
name,
baseUrl,
skipTlsVerify: Boolean((server as any).skipTlsVerify),
createdAt,
updatedAt,
lastConnectedAt,
}
}).sort((a, b) => {
const left = a.lastConnectedAt ?? a.updatedAt
const right = b.lastConnectedAt ?? b.updatedAt
return right.localeCompare(left)
}),
models: {
recents: cloneArray<ModelPreference>((source.models as any)?.recents, (m) => {
if (!m || typeof m !== "object") return null
@@ -311,6 +337,43 @@ function buildBinaryList(binaryPath: string, version: string | undefined, source
return [nextEntry, ...source].slice(0, 10)
}
interface RemoteServerProfileInput {
id?: string
name: string
baseUrl: string
skipTlsVerify: boolean
}
function buildRemoteServerProfile(input: RemoteServerProfileInput, source: RemoteServerProfile[]): RemoteServerProfile {
const existing = input.id ? source.find((entry) => entry.id === input.id) : undefined
const now = new Date().toISOString()
return {
id: existing?.id ?? input.id ?? createRandomId(),
name: input.name.trim(),
baseUrl: input.baseUrl.trim(),
skipTlsVerify: Boolean(input.skipTlsVerify),
createdAt: existing?.createdAt ?? now,
updatedAt: now,
lastConnectedAt: existing?.lastConnectedAt,
}
}
function buildRemoteServerList(profile: RemoteServerProfile, source: RemoteServerProfile[]): RemoteServerProfile[] {
const remaining = source.filter((entry) => entry.id !== profile.id)
return [profile, ...remaining].sort((a, b) => {
const left = a.lastConnectedAt ?? a.updatedAt
const right = b.lastConnectedAt ?? b.updatedAt
return right.localeCompare(left)
})
}
function createRandomId(): string {
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
return crypto.randomUUID()
}
return `remote-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
}
const [uiConfigBucket, setUiConfigBucket] = createSignal<UiConfigBucket>({})
const [serverConfigBucket, setServerConfigBucket] = createSignal<ServerConfigBucket>({})
const [uiStateBucket, setUiStateBucket] = createSignal<UiStateBucket>({})
@@ -324,6 +387,7 @@ const uiState = createMemo(() => normalizeUiState(uiStateBucket()))
const preferences = uiSettings
const recentFolders = createMemo<RecentFolder[]>(() => uiState().recentFolders)
const opencodeBinaries = createMemo<OpenCodeBinary[]>(() => uiState().opencodeBinaries)
const remoteServers = createMemo<RemoteServerProfile[]>(() => uiState().remoteServers)
let loadPromise: Promise<void> | null = null
@@ -467,6 +531,29 @@ function removeRecentFolder(folderPath: string): void {
void patchStateOwner("ui", { recentFolders: next }).catch((error) => log.error("Failed to remove recent folder", error))
}
async function saveRemoteServerProfile(input: RemoteServerProfileInput): Promise<RemoteServerProfile> {
const profile = buildRemoteServerProfile(input, remoteServers())
await patchStateOwner("ui", { remoteServers: buildRemoteServerList(profile, remoteServers()) })
return profile
}
async function markRemoteServerConnected(id: string): Promise<void> {
const current = remoteServers().find((entry) => entry.id === id)
if (!current) return
const now = new Date().toISOString()
const updated: RemoteServerProfile = {
...current,
updatedAt: now,
lastConnectedAt: now,
}
await patchStateOwner("ui", { remoteServers: buildRemoteServerList(updated, remoteServers()) })
}
function removeRemoteServerProfile(id: string): void {
const next = remoteServers().filter((entry) => entry.id !== id)
void patchStateOwner("ui", { remoteServers: next }).catch((error) => log.error("Failed to remove remote server", error))
}
function recordWorkspaceLaunch(folderPath: string, binaryPath?: string): void {
const targetBinary = binaryPath && binaryPath.trim().length > 0 ? binaryPath : serverSettings().opencodeBinary
const nextFolders = buildRecentFolderList(folderPath, recentFolders())
@@ -630,11 +717,15 @@ interface ConfigContextValue {
// ui-owned state
recentFolders: typeof recentFolders
opencodeBinaries: typeof opencodeBinaries
remoteServers: typeof remoteServers
uiState: typeof uiState
addRecentFolder: typeof addRecentFolder
removeRecentFolder: typeof removeRecentFolder
addOpenCodeBinary: typeof addOpenCodeBinary
removeOpenCodeBinary: typeof removeOpenCodeBinary
saveRemoteServerProfile: typeof saveRemoteServerProfile
markRemoteServerConnected: typeof markRemoteServerConnected
removeRemoteServerProfile: typeof removeRemoteServerProfile
recordWorkspaceLaunch: typeof recordWorkspaceLaunch
addRecentModelPreference: typeof addRecentModelPreference
isFavoriteModelPreference: typeof isFavoriteModelPreference
@@ -679,11 +770,15 @@ const configContextValue: ConfigContextValue = {
updateSpeechSettings,
recentFolders,
opencodeBinaries,
remoteServers,
uiState,
addRecentFolder,
removeRecentFolder,
addOpenCodeBinary,
removeOpenCodeBinary,
saveRemoteServerProfile,
markRemoteServerConnected,
removeRemoteServerProfile,
recordWorkspaceLaunch,
addRecentModelPreference,
isFavoriteModelPreference,