From 3bf054c1d9c30b6f294def81da735a6c15cb3d07 Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Sat, 23 May 2026 10:40:47 +1000 Subject: [PATCH] fix(app): restore desktop prod legacy flows (#28919) --- .../src/context/global-sync/bootstrap.test.ts | 78 ++++++ .../app/src/context/global-sync/bootstrap.ts | 3 + .../context/global-sync/child-store.test.ts | 94 ++++++- .../src/context/global-sync/child-store.ts | 2 +- packages/app/src/pages/home.tsx | 238 +++++------------- packages/app/src/pages/session.tsx | 4 +- .../pages/session/new-session-layout.test.ts | 14 ++ .../src/pages/session/new-session-layout.ts | 3 + 8 files changed, 248 insertions(+), 188 deletions(-) create mode 100644 packages/app/src/context/global-sync/bootstrap.test.ts create mode 100644 packages/app/src/pages/session/new-session-layout.test.ts create mode 100644 packages/app/src/pages/session/new-session-layout.ts diff --git a/packages/app/src/context/global-sync/bootstrap.test.ts b/packages/app/src/context/global-sync/bootstrap.test.ts new file mode 100644 index 0000000000..b75ae5cc32 --- /dev/null +++ b/packages/app/src/context/global-sync/bootstrap.test.ts @@ -0,0 +1,78 @@ +import { describe, expect, test } from "bun:test" +import { createStore } from "solid-js/store" +import { QueryClient } from "@tanstack/solid-query" +import type { Config, OpencodeClient, Project } from "@opencode-ai/sdk/v2/client" +import type { NormalizedProviderListResponse } from "@opencode-ai/ui/context" +import { bootstrapDirectory } from "./bootstrap" +import type { State, VcsCache } from "./types" + +const provider = { all: new Map(), connected: [], default: {} } satisfies NormalizedProviderListResponse + +describe("bootstrapDirectory", () => { + test("marks a loading directory partial during bootstrap and complete after success", async () => { + const [store, setStore] = createStore({ + status: "loading", + agent: [], + command: [], + project: "", + projectMeta: undefined, + icon: undefined, + provider_ready: true, + provider, + config: {}, + path: { state: "", config: "", worktree: "/project", directory: "/project", home: "/home" }, + session: [], + sessionTotal: 0, + session_status: {}, + session_working(id: string) { + return this.session_status[id]?.type !== "idle" + }, + session_diff: {}, + todo: {}, + permission: {}, + question: {}, + mcp_ready: true, + mcp: {}, + lsp_ready: true, + lsp: [], + vcs: undefined, + limit: 5, + message: {}, + part: {}, + part_text_accum_delta: {}, + }) + + await bootstrapDirectory({ + directory: "/project", + global: { + config: {} satisfies Config, + path: { state: "", config: "", worktree: "/project", directory: "/project", home: "/home" }, + project: [{ id: "project", worktree: "/project" } as Project], + provider, + }, + sdk: { + app: { agents: async () => ({ data: [{ name: "build", mode: "primary" }] }) }, + config: { get: async () => ({ data: {} }) }, + session: { status: async () => ({ data: {} }) }, + vcs: { get: async () => ({ data: undefined }) }, + command: { list: async () => ({ data: [] }) }, + permission: { list: async () => ({ data: [] }) }, + question: { list: async () => ({ data: [] }) }, + mcp: { status: async () => ({ data: {} }) }, + provider: { list: async () => ({ data: { all: [], connected: [], default: {} } }) }, + } as unknown as OpencodeClient, + store, + setStore, + vcsCache: { setStore() {} } as unknown as VcsCache, + loadSessions() {}, + translate: (key) => key, + queryClient: new QueryClient(), + }) + + expect(store.status).toBe("partial") + + await new Promise((resolve) => setTimeout(resolve, 80)) + + expect(store.status).toBe("complete") + }) +}) diff --git a/packages/app/src/context/global-sync/bootstrap.ts b/packages/app/src/context/global-sync/bootstrap.ts index 655f65a676..971932fb0a 100644 --- a/packages/app/src/context/global-sync/bootstrap.ts +++ b/packages/app/src/context/global-sync/bootstrap.ts @@ -220,6 +220,7 @@ export async function bootstrapDirectory(input: { if (Object.keys(input.store.config).length === 0 && Object.keys(input.global.config).length > 0) { input.setStore("config", reconcile(input.global.config, { merge: false })) } + if (loading) input.setStore("status", "partial") const rev = (providerRev.get(input.directory) ?? 0) + 1 providerRev.set(input.directory, rev) @@ -326,5 +327,7 @@ export async function bootstrapDirectory(input: { description: formatServerError(slowErrs[0], input.translate), }) } + + if (loading && slowErrs.length === 0) input.setStore("status", "complete") })() } diff --git a/packages/app/src/context/global-sync/child-store.test.ts b/packages/app/src/context/global-sync/child-store.test.ts index bb8eb7ce7f..89d6ee1d8e 100644 --- a/packages/app/src/context/global-sync/child-store.test.ts +++ b/packages/app/src/context/global-sync/child-store.test.ts @@ -1,10 +1,63 @@ -import { describe, expect, test } from "bun:test" -import { createRoot, getOwner } from "solid-js" +import { beforeAll, describe, expect, mock, test } from "bun:test" +import { createRoot, getOwner, type Owner } from "solid-js" import { createStore } from "solid-js/store" +import type { NormalizedProviderListResponse } from "@opencode-ai/ui/context" import type { State } from "./types" -import { createChildStoreManager } from "./child-store" +import type { QueryOptionsApi } from "../global-sync" + +let createChildStoreManager: typeof import("./child-store").createChildStoreManager const child = () => createStore({} as State) +const provider = { all: new Map(), connected: [], default: {} } satisfies NormalizedProviderListResponse + +const queryOptionsApi = { + globalConfig: () => ({ queryKey: ["globalConfig"], queryFn: async () => ({}) }), + projects: () => ({ queryKey: ["projects"], queryFn: async () => [] }), + providers: (directory: string | null) => ({ queryKey: [directory, "providers"], queryFn: async () => provider }), + path: (directory: string | null) => ({ + queryKey: [directory, "path"], + queryFn: async () => ({ + state: "", + config: "", + worktree: "", + directory: directory ?? "", + home: "", + }), + }), + agents: (directory: string) => ({ queryKey: [directory, "agents"], queryFn: async () => [] }), + mcp: (directory: string) => ({ queryKey: [directory, "mcp"], queryFn: async () => ({}) }), + lsp: (directory: string) => ({ queryKey: [directory, "lsp"], queryFn: async () => [] }), + sessions: (directory: string) => ({ queryKey: [directory, "loadSessions"] as const }), +} as unknown as QueryOptionsApi + +function createOwner(callback: (owner: Owner) => void) { + return createRoot((dispose) => { + const owner = getOwner() + if (!owner) throw new Error("owner required") + callback(owner) + + return dispose + }) +} + +beforeAll(async () => { + mock.module("@/utils/persist", () => ({ + Persist: { + workspace: (...parts: string[]) => parts.join(":"), + }, + persisted: (_target: string, store: unknown[]) => [store[0], store[1], null, () => true], + })) + mock.module("@tanstack/solid-query", () => ({ + useQueries: () => [ + { isLoading: false, data: { state: "", config: "", worktree: "", directory: "", home: "" } }, + { isLoading: false, data: {} }, + { isLoading: false, data: [] }, + { isLoading: false, data: provider }, + ], + })) + + createChildStoreManager = (await import("./child-store")).createChildStoreManager +}) describe("createChildStoreManager", () => { test("does not evict the active directory during mark", () => { @@ -22,8 +75,8 @@ describe("createChildStoreManager", () => { onBootstrap() {}, onDispose() {}, translate: (key) => key, - queryOptions: {} as any, - global: { provider: null! }, + queryOptions: queryOptionsApi, + global: { provider }, }) Array.from({ length: 30 }, (_, index) => `/pinned-${index}`).forEach((directory) => { @@ -37,4 +90,35 @@ describe("createChildStoreManager", () => { expect(manager.children[directory]).toBeDefined() }) + + test("starts new child stores as loading and bootstraps them on first access", () => { + const bootstraps: string[] = [] + let manager: ReturnType | undefined + + const dispose = createOwner((owner) => { + manager = createChildStoreManager({ + owner, + isBooting: () => false, + isLoadingSessions: () => false, + onBootstrap(directory) { + bootstraps.push(directory) + }, + onDispose() {}, + translate: (key) => key, + queryOptions: queryOptionsApi, + global: { provider }, + }) + }) + + try { + if (!manager) throw new Error("manager required") + + const [store] = manager.child("/project") + + expect(store.status).toBe("loading") + expect(bootstraps).toEqual(["/project"]) + } finally { + dispose() + } + }) }) diff --git a/packages/app/src/context/global-sync/child-store.ts b/packages/app/src/context/global-sync/child-store.ts index 56935ccc99..40c3c3ae92 100644 --- a/packages/app/src/context/global-sync/child-store.ts +++ b/packages/app/src/context/global-sync/child-store.ts @@ -202,7 +202,7 @@ export function createChildStoreManager(input: { return { state: "", config: "", worktree: "", directory: "", home: "" } return pathQuery.data }, - status: "complete" as const, + status: "loading" as const, agent: [], command: [], session: [], diff --git a/packages/app/src/pages/home.tsx b/packages/app/src/pages/home.tsx index 0329568847..00c6cd3ca6 100644 --- a/packages/app/src/pages/home.tsx +++ b/packages/app/src/pages/home.tsx @@ -1,5 +1,5 @@ import type { Session } from "@opencode-ai/sdk/v2/client" -import { createMemo, createSignal, For, Match, Show, Switch } from "solid-js" +import { createMemo, For, Match, Show, Switch } from "solid-js" import { createStore } from "solid-js/store" import { useQuery } from "@tanstack/solid-query" import { Button } from "@opencode-ai/ui/button" @@ -18,7 +18,6 @@ import { DateTime } from "luxon" import { useDialog } from "@opencode-ai/ui/context/dialog" import { DialogSelectDirectory } from "@/components/dialog-select-directory" import { DialogSelectServer } from "@/components/dialog-select-server" -import { DialogSelectModel } from "@/components/dialog-select-model" import { useServer } from "@/context/server" import { useGlobalSync } from "@/context/global-sync" import { useLanguage } from "@/context/language" @@ -467,11 +466,6 @@ function LegacyHome() { const navigate = useNavigate() const server = useServer() const language = useLanguage() - - const [promptText, setPromptText] = createSignal("") - const [selectedAgent, setSelectedAgent] = createSignal("frontend-specialist") - const [showProjectsDropdown, setShowProjectsDropdown] = createSignal(false) - const homedir = createMemo(() => sync.data.path.home) const recent = createMemo(() => { return sync.data.project @@ -480,8 +474,6 @@ function LegacyHome() { .slice(0, 5) }) - const currentProject = createMemo(() => recent()[0]?.worktree) - const serverDotClass = createMemo(() => { const healthy = server.healthy() if (healthy === true) return "bg-icon-success-base" @@ -520,185 +512,69 @@ function LegacyHome() { } } - function handleModelSelect() { - dialog.show(() => ) - } - - function toggleAgent() { - const agents = ["frontend-specialist", "build", "general"] - const nextIndex = (agents.indexOf(selectedAgent()) + 1) % agents.length - setSelectedAgent(agents[nextIndex]) - } - - function handleSubmit() { - const projectToOpen = currentProject() - if (projectToOpen) { - openProject(projectToOpen) - } else { - chooseProject() - } - } - - const activeModelName = createMemo(() => { - const model = sync.data.config.model - if (!model) return "GPT-5.7 Pro" - const parts = model.split("/") - return parts[parts.length - 1] - }) - return ( -
-
-
- -
- -
- +
+ + - 0}> -
-
{language.t("session.new.title")}
- -
-