feat(app): improve desktop multi-server support (#30678)
Co-authored-by: Brendan Allan <git@brendonovich.dev>
This commit is contained in:
co-authored by
Brendan Allan
parent
7ae856a9e9
commit
7f33576f46
@@ -9,13 +9,21 @@ import {
|
||||
import { type Session } from "@opencode-ai/sdk/v2/client"
|
||||
import {
|
||||
childSessionOnPath,
|
||||
closeHomeProject,
|
||||
displayName,
|
||||
effectiveWorkspaceOrder,
|
||||
errorMessage,
|
||||
hasProjectPermissions,
|
||||
homeProjectNavigation,
|
||||
homeProjectDirectories,
|
||||
homeSessionServerStatus,
|
||||
latestRootSession,
|
||||
toggleHomeProjectSelection,
|
||||
} from "./helpers"
|
||||
import { pathKey } from "@/utils/path-key"
|
||||
import { ServerConnection } from "@/context/server"
|
||||
|
||||
const serverKey = ServerConnection.Key.make
|
||||
|
||||
const session = (input: Partial<Session> & Pick<Session, "id" | "directory">) =>
|
||||
({
|
||||
@@ -215,6 +223,84 @@ describe("layout workspace helpers", () => {
|
||||
test("formats fallback project display name", () => {
|
||||
expect(displayName({ worktree: "/tmp/app" })).toBe("app")
|
||||
expect(displayName({ worktree: "/tmp/app", name: "My App" })).toBe("My App")
|
||||
expect(displayName({ worktree: "/" })).toBe("/")
|
||||
})
|
||||
|
||||
test("scopes home project selection by server", () => {
|
||||
expect(toggleHomeProjectSelection(undefined, serverKey("https://debian.example"), "/home/luke/repos/amazon")).toEqual({
|
||||
server: serverKey("https://debian.example"),
|
||||
directory: "/home/luke/repos/amazon",
|
||||
})
|
||||
expect(
|
||||
toggleHomeProjectSelection(
|
||||
{ server: serverKey("https://windows.example"), directory: "/home/luke/repos/amazon" },
|
||||
serverKey("https://debian.example"),
|
||||
"/home/luke/repos/amazon",
|
||||
),
|
||||
).toEqual({ server: serverKey("https://debian.example"), directory: "/home/luke/repos/amazon" })
|
||||
expect(
|
||||
toggleHomeProjectSelection(
|
||||
{ server: serverKey("https://debian.example"), directory: "/home/luke/repos/amazon" },
|
||||
serverKey("https://debian.example"),
|
||||
"/home/luke/repos/amazon",
|
||||
),
|
||||
).toEqual({ server: serverKey("https://debian.example") })
|
||||
})
|
||||
|
||||
test("closes a home project through its server context", () => {
|
||||
const closed: string[] = []
|
||||
|
||||
expect(
|
||||
closeHomeProject(
|
||||
{ server: serverKey("https://windows.example"), directory: "/shared" },
|
||||
serverKey("https://debian.example"),
|
||||
{ close: (directory) => closed.push(directory) },
|
||||
"/shared",
|
||||
),
|
||||
).toEqual({ server: serverKey("https://windows.example"), directory: "/shared" })
|
||||
expect(closed).toEqual(["/shared"])
|
||||
expect(
|
||||
closeHomeProject(
|
||||
{ server: serverKey("https://debian.example"), directory: "/shared" },
|
||||
serverKey("https://debian.example"),
|
||||
{ close: (directory) => closed.push(directory) },
|
||||
"/shared",
|
||||
),
|
||||
).toEqual({ server: serverKey("https://debian.example") })
|
||||
})
|
||||
|
||||
test("defers home project navigation until its server is active", () => {
|
||||
expect(homeProjectNavigation(serverKey("sidecar"), serverKey("https://debian.example"), "/YW1hem9u/session")).toEqual({
|
||||
server: serverKey("https://debian.example"),
|
||||
href: "/YW1hem9u/session",
|
||||
})
|
||||
expect(homeProjectNavigation(serverKey("https://debian.example"), serverKey("https://debian.example"), "/YW1hem9u/session")).toEqual({
|
||||
href: "/YW1hem9u/session",
|
||||
})
|
||||
})
|
||||
|
||||
test("preserves picker order when adding multiple projects", () => {
|
||||
expect(homeProjectDirectories(["/first", "/second"])).toEqual(["/first", "/second"])
|
||||
expect(homeProjectDirectories("/only")).toEqual(["/only"])
|
||||
expect(homeProjectDirectories(null)).toEqual([])
|
||||
})
|
||||
|
||||
test("hides status derived from an inactive server", () => {
|
||||
let reads = 0
|
||||
const status = () => {
|
||||
reads++
|
||||
return { working: true, tint: "red" }
|
||||
}
|
||||
expect(homeSessionServerStatus(false, status)).toEqual({
|
||||
working: false,
|
||||
tint: undefined,
|
||||
})
|
||||
expect(reads).toBe(0)
|
||||
expect(homeSessionServerStatus(true, status)).toEqual({
|
||||
working: true,
|
||||
tint: "red",
|
||||
})
|
||||
expect(reads).toBe(1)
|
||||
})
|
||||
|
||||
test("extracts api error message and fallback", () => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { getFilename } from "@opencode-ai/core/util/path"
|
||||
import { type Session } from "@opencode-ai/sdk/v2/client"
|
||||
import { pathKey } from "@/utils/path-key"
|
||||
import type { ServerConnection } from "@/context/server"
|
||||
|
||||
type SessionStore = {
|
||||
session?: Session[]
|
||||
@@ -53,7 +54,44 @@ export const childSessionOnPath = (sessions: Session[] | undefined, rootID: stri
|
||||
}
|
||||
|
||||
export const displayName = (project: { name?: string; worktree: string }) =>
|
||||
project.name || getFilename(project.worktree)
|
||||
project.name || getFilename(project.worktree) || project.worktree
|
||||
|
||||
export type HomeProjectSelection = { server: ServerConnection.Key; directory?: string }
|
||||
|
||||
export function toggleHomeProjectSelection(
|
||||
current: HomeProjectSelection | undefined,
|
||||
server: ServerConnection.Key,
|
||||
directory: string,
|
||||
): HomeProjectSelection {
|
||||
if (current?.server === server && current.directory === directory) return { server }
|
||||
return { server, directory }
|
||||
}
|
||||
|
||||
export function closeHomeProject(
|
||||
selected: HomeProjectSelection | undefined,
|
||||
server: ServerConnection.Key,
|
||||
projects: { close: (directory: string) => void },
|
||||
directory: string,
|
||||
) {
|
||||
projects.close(directory)
|
||||
if (selected?.server === server && selected.directory === directory) return { server }
|
||||
return selected
|
||||
}
|
||||
|
||||
export function homeProjectNavigation(active: ServerConnection.Key, server: ServerConnection.Key, href: string) {
|
||||
if (active === server) return { href }
|
||||
return { server, href }
|
||||
}
|
||||
|
||||
export function homeProjectDirectories(result: string | string[] | null) {
|
||||
if (!result) return []
|
||||
return Array.isArray(result) ? result : [result]
|
||||
}
|
||||
|
||||
export function homeSessionServerStatus(active: boolean, status: () => { working: boolean; tint?: string }) {
|
||||
if (!active) return { working: false, tint: undefined }
|
||||
return status()
|
||||
}
|
||||
|
||||
const OPENCODE_PROJECT_ID = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
|
||||
|
||||
|
||||
@@ -4,18 +4,20 @@ import { useNotification } from "@/context/notification"
|
||||
import { usePermission } from "@/context/permission"
|
||||
import { sessionPermissionRequest } from "@/pages/session/composer/session-request-tree"
|
||||
|
||||
export function useSessionTabAvatarState(directory: Accessor<string>, sessionId: Accessor<string>) {
|
||||
export function useSessionTabAvatarState(directory: Accessor<string>, sessionId: Accessor<string>, active: Accessor<boolean> = () => true) {
|
||||
const globalSync = useServerSync()
|
||||
const notification = useNotification()
|
||||
const permission = usePermission()
|
||||
const hasPermissions = createMemo(() => {
|
||||
if (!active()) return false
|
||||
const [store] = globalSync.child(directory(), { bootstrap: false })
|
||||
return !!sessionPermissionRequest(store.session, store.permission, sessionId(), (item) => {
|
||||
return !permission.autoResponds(item, directory())
|
||||
})
|
||||
})
|
||||
const unread = createMemo(() => hasPermissions() || notification.session.unseenCount(sessionId()) > 0)
|
||||
const unread = createMemo(() => active() && (hasPermissions() || notification.session.unseenCount(sessionId()) > 0))
|
||||
const loading = createMemo(() => {
|
||||
if (!active()) return false
|
||||
if (hasPermissions()) return false
|
||||
const [store] = globalSync.child(directory(), { bootstrap: false })
|
||||
return store.session_working(sessionId())
|
||||
|
||||
Reference in New Issue
Block a user