From e848b8a96e6159a9d048609575cb7dc00d0ade31 Mon Sep 17 00:00:00 2001 From: Yuhan Lei Date: Wed, 3 Jun 2026 09:40:30 +0800 Subject: [PATCH] refactor(app): extract homepage migration effect to layout-homepage-migration.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Slice 12 of #1056 layout governance line — pure extraction. Moves the fire-and-forget v7 homepage-draft migration createEffect out of pages/layout.tsx into a new useHomepageMigration({ currentDir, platform }) factory, mirroring the sibling useUpdatePolling / layout-update-polling.ts injection pattern. The effect registers in the parent reactive root exactly as before (call sited where the effect previously lived), so owner and registration order are unchanged. Five now-unused imports drop from layout.tsx (runHomepageMigration, HOMEPAGE_MIGRATION_SENTINEL_KEY, LegacyHomepagePromptStore, usePortableDraft, createMigrationStorageIO); the new file imports them directly. Persist and createEffect stay in layout.tsx (still used elsewhere). No behaviour / DOM / aria / copy / storage-key change — the effect body, sentinel handling, idempotent guard, and the desktop-vs-web storage branch (which already lived in homepage-migration-storage.ts, not layout.tsx) are byte-for-byte identical. layout.tsx 1084 -> 1026 (-58). ## Verification - bun run typecheck clean - Source-guard tests (shell-frame-contract, update-install-flow-source) + homepage-migration logic tests: 27 pass (the guards make only negative assertions unrelated to migration, so none needed to move) - Full bun run test:unit: 1734 pass / 0 fail (no regression) No new unit test: the extracted unit is a pure effect wrapper with no memo / pure-helper paths, and bun's solid-js server build no-ops createEffect so the effect body is not drivable under bun (same constraint as slice 10). The migration logic it wires is already covered by homepage-migration.test.ts; equivalence of the move is verified by codex review. --- packages/app/src/pages/layout.tsx | 62 +---------------- .../pages/layout/layout-homepage-migration.ts | 66 +++++++++++++++++++ 2 files changed, 68 insertions(+), 60 deletions(-) create mode 100644 packages/app/src/pages/layout/layout-homepage-migration.ts diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 61f5bff25..670a62191 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -38,13 +38,6 @@ import { setNavigate } from "@/utils/notification-click" import { setOpenSettings } from "@/utils/settings-navigation" import { Worktree as WorktreeState } from "@/utils/worktree" import { usePinnedDraft } from "@/components/prompt-input/pinned-draft" -import { - runHomepageMigration, - HOMEPAGE_MIGRATION_SENTINEL_KEY, - type LegacyHomepagePromptStore, -} from "@/components/prompt-input/homepage-migration" -import { usePortableDraft } from "@/components/prompt-input/portable-draft" -import { createMigrationStorageIO } from "@/components/prompt-input/homepage-migration-storage" import { useDialog } from "@opencode-ai/ui/context/dialog" import { useTheme } from "@opencode-ai/ui/theme/context" @@ -64,6 +57,7 @@ import { pawworkSessionDirectories } from "./layout/pawwork-session-source" import { findPawworkSessionNavigationTarget } from "./layout/pawwork-session-nav" import { createShellNavigation } from "./layout/shell-navigation" import { useUpdatePolling } from "./layout/layout-update-polling" +import { useHomepageMigration } from "./layout/layout-homepage-migration" import { sessionNotificationHref, useSDKNotificationToasts } from "./layout/layout-sdk-event-effects" import { registerLayoutCommands } from "./layout/layout-commands" import { LayoutShellFrame } from "./layout/layout-shell-frame" @@ -681,59 +675,7 @@ export default function Layout(props: ParentProps) { resetWorkspace, }) - // Run the v7 homepage-draft migration as soon as a directory becomes - // available (fire-and-forget). currentDir() can be empty during the initial - // autoselect phase, so onMount alone would skip migration for that session. - // The migration writes a sentinel internally and is idempotent, so subsequent - // effect ticks are no-ops once it has run. - let homepageMigrationStarted = false - createEffect(() => { - if (homepageMigrationStarted) return - const directory = currentDir() - if (!directory) return - homepageMigrationStarted = true - - const portable = usePortableDraft() - const sentinelTarget = Persist.global(HOMEPAGE_MIGRATION_SENTINEL_KEY) - const { read: readRaw, write: writeRaw, remove: removeRaw } = createMigrationStorageIO(platform) - - void runHomepageMigration({ - portable, - currentDirectory: directory, - readSentinel: async () => { - const raw = await readRaw(sentinelTarget) - if (!raw) return null - try { - return JSON.parse(raw) as import("@/components/prompt-input/homepage-migration").MigrationSentinel - } catch { - return null - } - }, - writeSentinel: async (sentinel) => { - await writeRaw(sentinelTarget, JSON.stringify(sentinel)) - }, - loadLegacyHomepage: async (dir) => { - const target = Persist.workspace(dir, "prompt") - const raw = await readRaw(target) - if (!raw) return null - try { - return JSON.parse(raw) as LegacyHomepagePromptStore - } catch { - return null - } - }, - clearLegacyHomepage: async (dir) => { - // Must await: desktop removeItem is async and a rejection here must - // propagate up to homepage-migration's failed-sentinel path. Without - // the await, the migration would write status: "complete" even if - // the legacy store delete failed. - await removeRaw(Persist.workspace(dir, "prompt")) - }, - }).catch((err) => { - // Log diagnostic; migration retries automatically on next boot. - console.warn("[homepage-migration] unexpected failure", err) - }) - }) + useHomepageMigration({ currentDir, platform }) async function renameProject(project: LocalProject, next: string) { const current = displayName(project) diff --git a/packages/app/src/pages/layout/layout-homepage-migration.ts b/packages/app/src/pages/layout/layout-homepage-migration.ts new file mode 100644 index 000000000..992baa5bf --- /dev/null +++ b/packages/app/src/pages/layout/layout-homepage-migration.ts @@ -0,0 +1,66 @@ +import { createEffect } from "solid-js" +import type { Platform } from "@/context/platform" +import { Persist } from "@/utils/persist" +import { + runHomepageMigration, + HOMEPAGE_MIGRATION_SENTINEL_KEY, + type LegacyHomepagePromptStore, +} from "@/components/prompt-input/homepage-migration" +import { usePortableDraft } from "@/components/prompt-input/portable-draft" +import { createMigrationStorageIO } from "@/components/prompt-input/homepage-migration-storage" + +export function useHomepageMigration(input: { currentDir: () => string; platform: Platform }) { + // Run the v7 homepage-draft migration as soon as a directory becomes + // available (fire-and-forget). currentDir() can be empty during the initial + // autoselect phase, so onMount alone would skip migration for that session. + // The migration writes a sentinel internally and is idempotent, so subsequent + // effect ticks are no-ops once it has run. + let homepageMigrationStarted = false + createEffect(() => { + if (homepageMigrationStarted) return + const directory = input.currentDir() + if (!directory) return + homepageMigrationStarted = true + + const portable = usePortableDraft() + const sentinelTarget = Persist.global(HOMEPAGE_MIGRATION_SENTINEL_KEY) + const { read: readRaw, write: writeRaw, remove: removeRaw } = createMigrationStorageIO(input.platform) + + void runHomepageMigration({ + portable, + currentDirectory: directory, + readSentinel: async () => { + const raw = await readRaw(sentinelTarget) + if (!raw) return null + try { + return JSON.parse(raw) as import("@/components/prompt-input/homepage-migration").MigrationSentinel + } catch { + return null + } + }, + writeSentinel: async (sentinel) => { + await writeRaw(sentinelTarget, JSON.stringify(sentinel)) + }, + loadLegacyHomepage: async (dir) => { + const target = Persist.workspace(dir, "prompt") + const raw = await readRaw(target) + if (!raw) return null + try { + return JSON.parse(raw) as LegacyHomepagePromptStore + } catch { + return null + } + }, + clearLegacyHomepage: async (dir) => { + // Must await: desktop removeItem is async and a rejection here must + // propagate up to homepage-migration's failed-sentinel path. Without + // the await, the migration would write status: "complete" even if + // the legacy store delete failed. + await removeRaw(Persist.workspace(dir, "prompt")) + }, + }).catch((err) => { + // Log diagnostic; migration retries automatically on next boot. + console.warn("[homepage-migration] unexpected failure", err) + }) + }) +}