diff --git a/packages/app/e2e/app/home.spec.ts b/packages/app/e2e/app/home.spec.ts index eaa62cb36..1c6a729c3 100644 --- a/packages/app/e2e/app/home.spec.ts +++ b/packages/app/e2e/app/home.spec.ts @@ -1,50 +1,6 @@ import { test, expect } from "../fixtures" -import { openSidebar } from "../actions" import { promptSelector, sessionComposerDockSelector } from "../selectors" -test("@smoke root route renders the no-project empty state", async ({ page, backend }) => { - await page.route(`${backend.url}/path**`, (route) => - route.fulfill({ - contentType: "application/json", - body: JSON.stringify({ home: "", state: "", config: "", worktree: "", directory: "" }), - }), - ) - await page.route(`${backend.url}/project**`, (route) => - route.fulfill({ - contentType: "application/json", - body: JSON.stringify([]), - }), - ) - await page.addInitScript((url) => { - localStorage.setItem( - "pawwork.global.dat:server", - JSON.stringify({ list: [url], projects: {}, lastProject: {} }), - ) - localStorage.setItem("pawwork.settings.dat:defaultServerUrl", url) - localStorage.setItem("pawwork.global.dat:globalSync.project", JSON.stringify({ value: [] })) - }, backend.url) - - await page.goto("/") - - const home = page.locator('[data-component="session-new-home"]') - const main = page.getByRole("main") - const emptyTitle = page.getByText(/No recent projects|没有最近项目/) - const emptyDescription = page.getByText(/Get started by opening a local project|通过打开本地项目开始使用/) - const openProject = main.getByRole("button", { name: /Open project|打开项目/ }) - - await expect(home).toHaveCount(0) - await expect(emptyTitle).toBeVisible() - await expect(emptyDescription).toBeVisible() - await expect(openProject).toBeVisible() - await expect(page.locator(sessionComposerDockSelector)).toHaveCount(0) - - await openSidebar(page) - await expect(page.getByText(/No projects open|还没有打开项目/)).toBeVisible() - await expect(page.getByText(/Open a project to start using the session-first sidebar\.|打开一个项目后,即可使用以会话为中心的侧边栏。/)).toBeVisible() - await openProject.click() - await expect(page.getByRole("dialog", { name: /Open project|打开项目/ })).toBeVisible() -}) - test("@smoke home renders the hero composer and starter cards", async ({ page, project }) => { await project.open() diff --git a/packages/app/e2e/app/root-redirect.spec.ts b/packages/app/e2e/app/root-redirect.spec.ts new file mode 100644 index 000000000..d3c25363b --- /dev/null +++ b/packages/app/e2e/app/root-redirect.spec.ts @@ -0,0 +1,18 @@ +import { test, expect } from "../fixtures" + +test("@smoke root route falls back to backend project when local store is empty", async ({ page, project }) => { + await project.open() + + await page.evaluate(() => { + const key = "pawwork.global.dat:server" + const raw = localStorage.getItem(key) + if (!raw) return + const parsed = JSON.parse(raw) as { projects?: Record } + parsed.projects = {} + localStorage.setItem(key, JSON.stringify(parsed)) + }) + + await page.goto("/") + + await expect(page).toHaveURL(/\/[A-Za-z0-9_-]+\/session/) +}) diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index c5862caaf..510dba004 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -31,10 +31,10 @@ import { CommandProvider } from "@/context/command" import { CommentsProvider } from "@/context/comments" import { FileProvider } from "@/context/file" import { GlobalSDKProvider } from "@/context/global-sdk" -import { GlobalSyncProvider } from "@/context/global-sync" +import { GlobalSyncProvider, useGlobalSync } from "@/context/global-sync" import { HighlightsProvider } from "@/context/highlights" import { LanguageProvider, type Locale, useLanguage } from "@/context/language" -import { LayoutProvider } from "@/context/layout" +import { LayoutProvider, useLayout } from "@/context/layout" import { ModelsProvider } from "@/context/models" import { NotificationProvider } from "@/context/notification" import { PermissionProvider } from "@/context/permission" @@ -49,8 +49,8 @@ import { ErrorPage } from "./pages/error" import { buildDesktopContext, desktopWindowTitle, type DesktopContext } from "./utils/desktop-context" import type { RendererDiagnosticInput, RendererDiagnosticsExportResult } from "@/context/platform" import { useCheckServerHealth } from "./utils/server-health" +import { base64Encode } from "@opencode-ai/util/encode" -const HomeRoute = lazy(() => import("@/pages/home")) const loadSession = () => import("@/pages/session") const Session = lazy(loadSession) const Loading = () =>
@@ -67,6 +67,25 @@ const SessionRoute = () => ( const SessionIndexRoute = () => +const HomeRedirectRoute = () => { + const layout = useLayout() + const sync = useGlobalSync() + const target = createMemo(() => { + const local = layout.projects.list()[0]?.worktree + if (local) return local + if (!sync.ready) return undefined + return sync.data.project[0]?.worktree + }) + return ( + + ) +} + type WebSearchStatus = { source: "saved" | "env" | "anonymous" configured: boolean @@ -406,7 +425,7 @@ export function AppInterface(props: { component={props.router ?? Router} root={(routerProps) => {routerProps.children}} > - + diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index dd628548c..0b5d14057 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -596,8 +596,6 @@ export const dict = { "notification.session.error.fallbackDescription": "An error occurred", "home.recentProjects": "Recent projects", - "home.empty.title": "No recent projects", - "home.empty.description": "Get started by opening a local project", "session.tab.session": "Session", "session.tab.review": "Review", diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index 7cc334b3a..1f0eae11b 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -565,8 +565,6 @@ export const dict = { "notification.session.error.fallbackDescription": "发生错误", "home.recentProjects": "最近项目", - "home.empty.title": "没有最近项目", - "home.empty.description": "通过打开本地项目开始使用", "session.tab.session": "会话", "session.tab.review": "审查", diff --git a/packages/app/src/pages/home.tsx b/packages/app/src/pages/home.tsx deleted file mode 100644 index eded98aa2..000000000 --- a/packages/app/src/pages/home.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { createMemo, For, Match, Switch } from "solid-js" -import { Button } from "@opencode-ai/ui/button" -import { Logo } from "@opencode-ai/ui/logo" -import { useLayout } from "@/context/layout" -import { useNavigate } from "@solidjs/router" -import { base64Encode } from "@opencode-ai/util/encode" -import { Icon } from "@opencode-ai/ui/icon" -import { usePlatform } from "@/context/platform" -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 { useServer } from "@/context/server" -import { useGlobalSync } from "@/context/global-sync" -import { useLanguage } from "@/context/language" - -export default function Home() { - const sync = useGlobalSync() - const layout = useLayout() - const platform = usePlatform() - const dialog = useDialog() - const navigate = useNavigate() - const server = useServer() - const language = useLanguage() - const homedir = createMemo(() => sync.data.path.home) - const recent = createMemo(() => { - return sync.data.project - .slice() - .sort((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created)) - .slice(0, 5) - }) - - const serverDotClass = createMemo(() => { - const healthy = server.healthy() - if (healthy === true) return "bg-icon-success-base" - if (healthy === false) return "bg-error" - return "bg-border-weak" - }) - - function openProject(directory: string) { - layout.projects.open(directory) - server.projects.touch(directory) - navigate(`/${base64Encode(directory)}`) - } - - async function chooseProject() { - function resolve(result: string | string[] | null) { - if (Array.isArray(result)) { - for (const directory of result) { - openProject(directory) - } - } else if (result) { - openProject(result) - } - } - - if (platform.openDirectoryPickerDialog && server.isLocal()) { - const result = await platform.openDirectoryPickerDialog?.({ - title: language.t("command.project.open"), - multiple: true, - }) - resolve(result) - } else { - dialog.show( - () => , - () => resolve(null), - ) - } - } - - return ( -
- - - - 0}> -
-
-
{language.t("home.recentProjects")}
- -
-
    - - {(project) => ( - - )} - -
-
-
- -
-
{language.t("common.loading")}
- -
-
- -
- -
-
{language.t("home.empty.title")}
-
{language.t("home.empty.description")}
-
- -
-
-
-
- ) -} diff --git a/packages/opencode/test/config/e2e-smoke-tagging.test.ts b/packages/opencode/test/config/e2e-smoke-tagging.test.ts index 8d014c0db..eaf4daf2f 100644 --- a/packages/opencode/test/config/e2e-smoke-tagging.test.ts +++ b/packages/opencode/test/config/e2e-smoke-tagging.test.ts @@ -8,8 +8,8 @@ const expectedSmokeTests = [ "packages/app/e2e/app/home.spec.ts:@smoke home hero prompt starts a session", "packages/app/e2e/app/home.spec.ts:@smoke home renders the hero composer and starter cards", "packages/app/e2e/app/home.spec.ts:@smoke project home status panel can open the server picker dialog", - "packages/app/e2e/app/home.spec.ts:@smoke root route renders the no-project empty state", "packages/app/e2e/app/navigation.spec.ts:@smoke project route redirects to /session", + "packages/app/e2e/app/root-redirect.spec.ts:@smoke root route falls back to backend project when local store is empty", "packages/app/e2e/app/session.spec.ts:@smoke session composer matches home structure without docktray or agent control", "packages/app/e2e/app/shell-frame.spec.ts:@smoke shell frame exposes stable desktop hooks", "packages/app/e2e/files/file-tree.spec.ts:@smoke review tab no longer renders the legacy file-tree sub-panel",