fix(app): restore desktop prod legacy flows (#28919)
This commit is contained in:
@@ -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<State>({
|
||||||
|
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")
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -220,6 +220,7 @@ export async function bootstrapDirectory(input: {
|
|||||||
if (Object.keys(input.store.config).length === 0 && Object.keys(input.global.config).length > 0) {
|
if (Object.keys(input.store.config).length === 0 && Object.keys(input.global.config).length > 0) {
|
||||||
input.setStore("config", reconcile(input.global.config, { merge: false }))
|
input.setStore("config", reconcile(input.global.config, { merge: false }))
|
||||||
}
|
}
|
||||||
|
if (loading) input.setStore("status", "partial")
|
||||||
|
|
||||||
const rev = (providerRev.get(input.directory) ?? 0) + 1
|
const rev = (providerRev.get(input.directory) ?? 0) + 1
|
||||||
providerRev.set(input.directory, rev)
|
providerRev.set(input.directory, rev)
|
||||||
@@ -326,5 +327,7 @@ export async function bootstrapDirectory(input: {
|
|||||||
description: formatServerError(slowErrs[0], input.translate),
|
description: formatServerError(slowErrs[0], input.translate),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (loading && slowErrs.length === 0) input.setStore("status", "complete")
|
||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,63 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { beforeAll, describe, expect, mock, test } from "bun:test"
|
||||||
import { createRoot, getOwner } from "solid-js"
|
import { createRoot, getOwner, type Owner } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
|
import type { NormalizedProviderListResponse } from "@opencode-ai/ui/context"
|
||||||
import type { State } from "./types"
|
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 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", () => {
|
describe("createChildStoreManager", () => {
|
||||||
test("does not evict the active directory during mark", () => {
|
test("does not evict the active directory during mark", () => {
|
||||||
@@ -22,8 +75,8 @@ describe("createChildStoreManager", () => {
|
|||||||
onBootstrap() {},
|
onBootstrap() {},
|
||||||
onDispose() {},
|
onDispose() {},
|
||||||
translate: (key) => key,
|
translate: (key) => key,
|
||||||
queryOptions: {} as any,
|
queryOptions: queryOptionsApi,
|
||||||
global: { provider: null! },
|
global: { provider },
|
||||||
})
|
})
|
||||||
|
|
||||||
Array.from({ length: 30 }, (_, index) => `/pinned-${index}`).forEach((directory) => {
|
Array.from({ length: 30 }, (_, index) => `/pinned-${index}`).forEach((directory) => {
|
||||||
@@ -37,4 +90,35 @@ describe("createChildStoreManager", () => {
|
|||||||
|
|
||||||
expect(manager.children[directory]).toBeDefined()
|
expect(manager.children[directory]).toBeDefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("starts new child stores as loading and bootstraps them on first access", () => {
|
||||||
|
const bootstraps: string[] = []
|
||||||
|
let manager: ReturnType<typeof createChildStoreManager> | 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()
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ export function createChildStoreManager(input: {
|
|||||||
return { state: "", config: "", worktree: "", directory: "", home: "" }
|
return { state: "", config: "", worktree: "", directory: "", home: "" }
|
||||||
return pathQuery.data
|
return pathQuery.data
|
||||||
},
|
},
|
||||||
status: "complete" as const,
|
status: "loading" as const,
|
||||||
agent: [],
|
agent: [],
|
||||||
command: [],
|
command: [],
|
||||||
session: [],
|
session: [],
|
||||||
|
|||||||
+57
-181
@@ -1,5 +1,5 @@
|
|||||||
import type { Session } from "@opencode-ai/sdk/v2/client"
|
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 { createStore } from "solid-js/store"
|
||||||
import { useQuery } from "@tanstack/solid-query"
|
import { useQuery } from "@tanstack/solid-query"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
@@ -18,7 +18,6 @@ import { DateTime } from "luxon"
|
|||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { DialogSelectDirectory } from "@/components/dialog-select-directory"
|
import { DialogSelectDirectory } from "@/components/dialog-select-directory"
|
||||||
import { DialogSelectServer } from "@/components/dialog-select-server"
|
import { DialogSelectServer } from "@/components/dialog-select-server"
|
||||||
import { DialogSelectModel } from "@/components/dialog-select-model"
|
|
||||||
import { useServer } from "@/context/server"
|
import { useServer } from "@/context/server"
|
||||||
import { useGlobalSync } from "@/context/global-sync"
|
import { useGlobalSync } from "@/context/global-sync"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
@@ -467,11 +466,6 @@ function LegacyHome() {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const server = useServer()
|
const server = useServer()
|
||||||
const language = useLanguage()
|
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 homedir = createMemo(() => sync.data.path.home)
|
||||||
const recent = createMemo(() => {
|
const recent = createMemo(() => {
|
||||||
return sync.data.project
|
return sync.data.project
|
||||||
@@ -480,8 +474,6 @@ function LegacyHome() {
|
|||||||
.slice(0, 5)
|
.slice(0, 5)
|
||||||
})
|
})
|
||||||
|
|
||||||
const currentProject = createMemo(() => recent()[0]?.worktree)
|
|
||||||
|
|
||||||
const serverDotClass = createMemo(() => {
|
const serverDotClass = createMemo(() => {
|
||||||
const healthy = server.healthy()
|
const healthy = server.healthy()
|
||||||
if (healthy === true) return "bg-icon-success-base"
|
if (healthy === true) return "bg-icon-success-base"
|
||||||
@@ -520,185 +512,69 @@ function LegacyHome() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleModelSelect() {
|
|
||||||
dialog.show(() => <DialogSelectModel />)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
<div class="mx-auto mt-24 w-full max-w-2xl px-6 flex flex-col items-center">
|
<div class="mx-auto mt-55 w-full md:w-auto px-4">
|
||||||
<div class="flex flex-col items-center gap-3 mb-10">
|
<Logo class="md:w-xl opacity-12" />
|
||||||
<div onClick={chooseProject} class="cursor-pointer hover:opacity-25 transition-opacity duration-200">
|
<Button
|
||||||
<Logo class="w-48 opacity-15" />
|
size="large"
|
||||||
</div>
|
variant="ghost"
|
||||||
<Button
|
class="mt-4 mx-auto text-14-regular text-text-weak"
|
||||||
size="normal"
|
onClick={() => dialog.show(() => <DialogSelectServer />)}
|
||||||
variant="ghost"
|
>
|
||||||
class="text-12-regular text-text-weak px-3"
|
<div
|
||||||
onClick={() => dialog.show(() => <DialogSelectServer />)}
|
classList={{
|
||||||
>
|
"size-2 rounded-full": true,
|
||||||
<div
|
[serverDotClass()]: true,
|
||||||
classList={{
|
}}
|
||||||
"size-1.5 rounded-full mr-2": true,
|
/>
|
||||||
[serverDotClass()]: true,
|
{server.name}
|
||||||
}}
|
</Button>
|
||||||
/>
|
|
||||||
{server.name}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={recent().length > 0}>
|
<Match when={sync.data.project.length > 0}>
|
||||||
<div class="w-full flex flex-col items-center gap-6">
|
<div class="mt-20 w-full flex flex-col gap-4">
|
||||||
<div class="text-20-medium text-text-strong text-center">{language.t("session.new.title")}</div>
|
<div class="flex gap-2 items-center justify-between pl-3">
|
||||||
|
<div class="text-14-medium text-text-strong">{language.t("home.recentProjects")}</div>
|
||||||
<div class="w-full bg-surface-base border border-border-base rounded-xl p-4 flex flex-col gap-3 shadow-md relative">
|
<Button icon="folder-add-left" size="normal" class="pl-2 pr-3" onClick={chooseProject}>
|
||||||
<textarea
|
{language.t("command.project.open")}
|
||||||
class="bg-transparent border-none outline-none text-14-regular text-text-base placeholder-text-weak w-full resize-none h-20 focus:outline-none"
|
</Button>
|
||||||
placeholder="Ask anything, / for commands, @ for context..."
|
|
||||||
value={promptText()}
|
|
||||||
onInput={(e) => setPromptText(e.currentTarget.value)}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === "Enter" && !e.shiftKey) {
|
|
||||||
e.preventDefault()
|
|
||||||
handleSubmit()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2 pt-3 border-t border-border-weak-base">
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
variant="ghost"
|
|
||||||
class="text-12-medium text-text-weak hover:text-text-strong flex items-center gap-1.5 px-2.5 py-1 bg-surface-raised-base hover:bg-surface-raised-base-hover border border-border-weak-base rounded-md"
|
|
||||||
onClick={toggleAgent}
|
|
||||||
>
|
|
||||||
<Icon name="sliders" size="small" class="shrink-0" />
|
|
||||||
<span>Agent: {selectedAgent()}</span>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
variant="ghost"
|
|
||||||
class="text-12-medium text-text-weak hover:text-text-strong flex items-center gap-1.5 px-2.5 py-1 bg-surface-raised-base hover:bg-surface-raised-base-hover border border-border-weak-base rounded-md"
|
|
||||||
onClick={handleModelSelect}
|
|
||||||
>
|
|
||||||
<Icon name="brain" size="small" class="shrink-0" />
|
|
||||||
<span>Model: {activeModelName()}</span>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div class="relative">
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
variant="ghost"
|
|
||||||
class="text-12-medium text-text-weak hover:text-text-strong flex items-center gap-1.5 px-2.5 py-1 bg-surface-raised-base hover:bg-surface-raised-base-hover border border-border-weak-base rounded-md"
|
|
||||||
onClick={() => setShowProjectsDropdown(!showProjectsDropdown())}
|
|
||||||
>
|
|
||||||
<Icon name="folder" size="small" class="shrink-0" />
|
|
||||||
<span>Project: {currentProject() ? getFilename(currentProject()) : "Select Project"}</span>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Show when={showProjectsDropdown()}>
|
|
||||||
<div class="absolute left-0 mt-1 w-64 bg-surface-raised-base border border-border-base rounded-lg p-2 shadow-lg z-50 flex flex-col gap-1">
|
|
||||||
<div class="text-10-semibold text-text-weak px-2 py-1 uppercase tracking-wider">
|
|
||||||
{language.t("home.recentProjects")}
|
|
||||||
</div>
|
|
||||||
<For each={recent()}>
|
|
||||||
{(project) => (
|
|
||||||
<button
|
|
||||||
class="text-12-mono text-left px-2 py-1.5 hover:bg-surface-raised-base-hover rounded flex items-center justify-between w-full"
|
|
||||||
onClick={() => {
|
|
||||||
openProject(project.worktree)
|
|
||||||
setShowProjectsDropdown(false)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span class="truncate">{getFilename(project.worktree)}</span>
|
|
||||||
<span class="text-10-regular text-text-weak shrink-0 pl-2">
|
|
||||||
{DateTime.fromMillis(project.time.updated ?? project.time.created).toRelative()}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
<div class="border-t border-border-weak-base my-1" />
|
|
||||||
<button
|
|
||||||
class="text-12-medium text-text-strong text-left px-2 py-1.5 hover:bg-surface-raised-base-hover rounded flex items-center gap-2 w-full"
|
|
||||||
onClick={() => {
|
|
||||||
setShowProjectsDropdown(false)
|
|
||||||
chooseProject()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon name="folder-add-left" size="small" />
|
|
||||||
{language.t("command.project.open")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
variant="ghost"
|
|
||||||
class="text-12-medium text-text-weak flex items-center gap-1.5 px-2.5 py-1 bg-surface-raised-base border border-border-weak-base rounded-md cursor-default pointer-events-none"
|
|
||||||
>
|
|
||||||
<Icon name="branch" size="small" class="shrink-0" />
|
|
||||||
<span>Branch: dev</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<ul class="flex flex-col gap-2">
|
||||||
|
<For each={recent()}>
|
||||||
|
{(project) => (
|
||||||
|
<Button
|
||||||
|
size="large"
|
||||||
|
variant="ghost"
|
||||||
|
class="text-14-mono text-left justify-between px-3"
|
||||||
|
onClick={() => openProject(project.worktree)}
|
||||||
|
>
|
||||||
|
{project.worktree.replace(homedir(), "~")}
|
||||||
|
<div class="text-14-regular text-text-weak">
|
||||||
|
{DateTime.fromMillis(project.time.updated ?? project.time.created).toRelative()}
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</Match>
|
||||||
|
<Match when={!sync.ready}>
|
||||||
|
<div class="mt-30 mx-auto flex flex-col items-center gap-3">
|
||||||
|
<div class="text-12-regular text-text-weak">{language.t("common.loading")}</div>
|
||||||
|
<Button class="px-3" onClick={chooseProject}>
|
||||||
|
{language.t("command.project.open")}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Match>
|
</Match>
|
||||||
|
|
||||||
<Match when={true}>
|
<Match when={true}>
|
||||||
<div class="w-full flex flex-col items-center gap-6">
|
<div class="mt-30 mx-auto flex flex-col items-center gap-3">
|
||||||
<div class="text-20-medium text-text-strong text-center">{language.t("home.empty.title")}</div>
|
<Icon name="folder-add-left" size="large" />
|
||||||
|
<div class="flex flex-col gap-1 items-center justify-center">
|
||||||
<div class="w-full bg-surface-base border border-border-base rounded-xl p-4 flex flex-col gap-3 shadow-md">
|
<div class="text-14-medium text-text-strong">{language.t("home.empty.title")}</div>
|
||||||
<div class="text-14-regular text-text-weak w-full min-h-[4rem] cursor-pointer" onClick={chooseProject}>
|
<div class="text-12-regular text-text-weak">{language.t("home.empty.description")}</div>
|
||||||
Ask anything, / for commands, @ for context...
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2 pt-3 border-t border-border-weak-base">
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
variant="ghost"
|
|
||||||
class="text-12-medium text-text-weak hover:text-text-strong flex items-center gap-1.5 px-2.5 py-1 bg-surface-raised-base hover:bg-surface-raised-base-hover border border-border-weak-base rounded-md"
|
|
||||||
onClick={chooseProject}
|
|
||||||
>
|
|
||||||
<Icon name="folder" size="small" class="shrink-0" />
|
|
||||||
<span>Open project</span>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
variant="ghost"
|
|
||||||
class="text-12-medium text-text-weak hover:text-text-strong flex items-center gap-1.5 px-2.5 py-1 bg-surface-raised-base hover:bg-surface-raised-base-hover border border-border-weak-base rounded-md"
|
|
||||||
onClick={handleModelSelect}
|
|
||||||
>
|
|
||||||
<Icon name="brain" size="small" class="shrink-0" />
|
|
||||||
<span>Model: {activeModelName()}</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<Button class="px-3 mt-1" onClick={chooseProject}>
|
||||||
|
{language.t("command.project.open")}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ import { SessionSidePanel } from "@/pages/session/session-side-panel"
|
|||||||
import { TerminalPanel } from "@/pages/session/terminal-panel"
|
import { TerminalPanel } from "@/pages/session/terminal-panel"
|
||||||
import { useSessionCommands } from "@/pages/session/use-session-commands"
|
import { useSessionCommands } from "@/pages/session/use-session-commands"
|
||||||
import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll"
|
import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll"
|
||||||
|
import { shouldUseV2NewSessionPage } from "@/pages/session/new-session-layout"
|
||||||
import { Identifier } from "@/utils/id"
|
import { Identifier } from "@/utils/id"
|
||||||
import { diffs as list } from "@/utils/diffs"
|
import { diffs as list } from "@/utils/diffs"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
@@ -263,7 +264,8 @@ export default function Page() {
|
|||||||
|
|
||||||
const isDesktop = createMediaQuery("(min-width: 768px)")
|
const isDesktop = createMediaQuery("(min-width: 768px)")
|
||||||
const size = createSizing()
|
const size = createSizing()
|
||||||
const isV2NewSessionPage = () => import.meta.env.VITE_OPENCODE_CHANNEL === "prod" || !params.id
|
const isV2NewSessionPage = () =>
|
||||||
|
shouldUseV2NewSessionPage({ channel: import.meta.env.VITE_OPENCODE_CHANNEL, sessionID: params.id })
|
||||||
const desktopReviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened() && !isV2NewSessionPage())
|
const desktopReviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened() && !isV2NewSessionPage())
|
||||||
const desktopFileTreeOpen = createMemo(() => isDesktop() && layout.fileTree.opened() && !isV2NewSessionPage())
|
const desktopFileTreeOpen = createMemo(() => isDesktop() && layout.fileTree.opened() && !isV2NewSessionPage())
|
||||||
const desktopSidePanelOpen = createMemo(() => desktopReviewOpen() || desktopFileTreeOpen())
|
const desktopSidePanelOpen = createMemo(() => desktopReviewOpen() || desktopFileTreeOpen())
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { shouldUseV2NewSessionPage } from "./new-session-layout"
|
||||||
|
|
||||||
|
describe("shouldUseV2NewSessionPage", () => {
|
||||||
|
test("keeps prod session pages on the legacy layout", () => {
|
||||||
|
expect(shouldUseV2NewSessionPage({ channel: "prod", sessionID: "ses_123" })).toBe(false)
|
||||||
|
expect(shouldUseV2NewSessionPage({ channel: "prod" })).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("uses the v2 layout only for non-prod new-session pages", () => {
|
||||||
|
expect(shouldUseV2NewSessionPage({ channel: "dev" })).toBe(true)
|
||||||
|
expect(shouldUseV2NewSessionPage({ channel: "dev", sessionID: "ses_123" })).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export function shouldUseV2NewSessionPage(input: { channel?: "dev" | "beta" | "prod"; sessionID?: string }) {
|
||||||
|
return input.channel !== "prod" && !input.sessionID
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user