add remote server launcher flow
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user