From c75ece5dcf9f5ff26d23ca165de2043d35580129 Mon Sep 17 00:00:00 2001 From: Yuhan Lei Date: Tue, 2 Jun 2026 22:22:48 +0800 Subject: [PATCH] refactor(app): extract pawwork project/pinned controls from layout.tsx --- packages/app/src/pages/layout.tsx | 152 +++--------------- .../pages/layout/pawwork-project-controls.ts | 151 +++++++++++++++++ 2 files changed, 174 insertions(+), 129 deletions(-) create mode 100644 packages/app/src/pages/layout/pawwork-project-controls.ts diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index b1f9881ae..c5814390e 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -26,7 +26,7 @@ import { getFilename } from "@opencode-ai/util/path" import { Session } from "@opencode-ai/sdk/v2/client" import { usePlatform } from "@/context/platform" import { useSettings } from "@/context/settings" -import { createStore, produce, reconcile } from "solid-js/store" +import { createStore, produce } from "solid-js/store" import type { DragEvent } from "@thisbeyond/solid-dnd" import { useProviders } from "@/hooks/use-providers" import { showToast, toaster } from "@opencode-ai/ui/toast" @@ -75,16 +75,8 @@ import { drainPendingDeepLinks, } from "./layout/deep-links" import { createInlineEditorController } from "./layout/inline-editor" -import { - pawworkSessionRouteUnhideKeys, - pawworkSessionDirectories, - resolvePawworkProjectRenameTarget, -} from "./layout/pawwork-session-source" -import { - findPawworkSessionNavigationTarget, - reorderPawworkPinnedByVisible, - unpinPawworkSession, -} from "./layout/pawwork-session-nav" +import { pawworkSessionRouteUnhideKeys, 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 { sessionNotificationHref, useSDKNotificationToasts } from "./layout/layout-sdk-event-effects" @@ -92,6 +84,7 @@ import { registerLayoutCommands } from "./layout/layout-commands" import { LayoutShellFrame } from "./layout/layout-shell-frame" import { createPawworkSessionPrefetch } from "./layout/pawwork-session-prefetch" import { createPawworkSessionController } from "./layout/pawwork-session-controller" +import { createPawworkProjectControls } from "./layout/pawwork-project-controls" import { type WorkspaceSidebarContext } from "./layout/sidebar-workspace" import { PawworkSidebar, type PawworkSidebarSession } from "./layout/pawwork-sidebar" import { createDefaultLayoutPageState, createLayoutPagePersistTarget } from "./layout/layout-page-store" @@ -496,124 +489,25 @@ export default function Layout(props: ParentProps) { } } - function togglePinnedSession(sessionID: string) { - setStore("pawworkPinnedSessions", (current) => { - const next = current.filter((id) => id !== sessionID) - if (next.length !== current.length) return next - return [sessionID, ...current] - }) - } - - /** - * Cross-zone drag: All ⇄ Pinned with positional insert, or intra-Pinned - * reorder. `visiblePinnedIDs` is the rendered pinned order from the sidebar; - * `visibleTargetIndex` is a slot inside it. We translate to the raw - * pinned array so hidden / un-loaded pinned IDs keep their positions. - */ - function dragPawworkSession(input: { - sessionID: string - targetSection: "pinned" | "recent" - visiblePinnedIDs: string[] - visibleTargetIndex: number - }) { - setStore("pawworkPinnedSessions", (current) => { - if (input.targetSection === "recent") { - return unpinPawworkSession({ pinnedIDs: current, sourceID: input.sessionID }) - } - return reorderPawworkPinnedByVisible({ - pinnedIDs: current, - visiblePinnedIDs: input.visiblePinnedIDs, - sourceID: input.sessionID, - targetVisibleIndex: input.visibleTargetIndex, - }) - }) - } - - /** - * Menu-driven move up / down: keyboard-accessible reorder within the pinned - * zone. Operates on the visible pinned order so adjacency matches what the - * user sees; the helper reconciles back to the raw array. - */ - function movePinnedSessionByOne(input: { - sessionID: string - direction: "up" | "down" - visiblePinnedIDs: string[] - }) { - const visibleIndex = input.visiblePinnedIDs.indexOf(input.sessionID) - if (visibleIndex === -1) return - const offset = input.direction === "up" ? -1 : 1 - const nextVisibleIndex = Math.max(0, Math.min(input.visiblePinnedIDs.length - 1, visibleIndex + offset)) - if (nextVisibleIndex === visibleIndex) return - setStore("pawworkPinnedSessions", (current) => - reorderPawworkPinnedByVisible({ - pinnedIDs: current, - visiblePinnedIDs: input.visiblePinnedIDs, - sourceID: input.sessionID, - targetVisibleIndex: nextVisibleIndex, - }), - ) - } - - function setPawworkSortMode(mode: "time" | "project") { - setStore("pawworkSortMode", mode) - } - - function toggleProjectCollapsed(label: string) { - const current = store.pawworkProjectCollapsed - const next: Record = { ...current } - if (next[label]) delete next[label] - else next[label] = true - setStore("pawworkProjectCollapsed", reconcile(next)) - } - - function hideProject(projectKey: string) { - if (store.pawworkProjectHidden[projectKey]) return - setStore("pawworkProjectHidden", projectKey, true) - showToast({ - title: language.t("project.remove.toast.title"), - description: language.t("project.remove.toast.description"), - actions: [ - { - label: language.t("common.undo"), - onClick: () => unhideProject(projectKey), - }, - ], - }) - } - - function unhideProject(projectKey: string) { - if (!store.pawworkProjectHidden[projectKey]) return - setStore( - "pawworkProjectHidden", - produce((draft) => { - delete draft[projectKey] - }), - ) - } - - async function handleRenameProject(projectKey: string, next: string) { - const target = resolvePawworkProjectRenameTarget(projectKey, { - projects: layout.projects.list(), - sessions: pawworkSessionWindow().sessions, - }) - if (!target) return - - if (target.type === "project") { - await renameProject(target.project, next) - return - } - - setWorkspaceName(target.directory, next) - } - - function expandPawworkProjectGroup(label: string | undefined) { - if (!label) return - if (!store.pawworkProjectCollapsed[label]) return - - const next: Record = { ...store.pawworkProjectCollapsed } - delete next[label] - setStore("pawworkProjectCollapsed", reconcile(next)) - } + const { + togglePinnedSession, + dragPawworkSession, + movePinnedSessionByOne, + setPawworkSortMode, + toggleProjectCollapsed, + hideProject, + unhideProject, + handleRenameProject, + expandPawworkProjectGroup, + } = createPawworkProjectControls({ + store, + setStore, + language, + projects: () => layout.projects.list(), + sessions: () => pawworkSessionWindow().sessions, + renameProject, + setWorkspaceName, + }) // Export hits the embedded sidecar via main-process IPC. When the user has // switched the active server to a remote target, the sidecar holds different diff --git a/packages/app/src/pages/layout/pawwork-project-controls.ts b/packages/app/src/pages/layout/pawwork-project-controls.ts new file mode 100644 index 000000000..0eaf20856 --- /dev/null +++ b/packages/app/src/pages/layout/pawwork-project-controls.ts @@ -0,0 +1,151 @@ +import { produce, reconcile, type SetStoreFunction } from "solid-js/store" +import { showToast } from "@opencode-ai/ui/toast" +import type { LocalProject } from "@/context/layout" +import { reorderPawworkPinnedByVisible, unpinPawworkSession } from "./pawwork-session-nav" +import { resolvePawworkProjectRenameTarget } from "./pawwork-session-source" +import { createDefaultLayoutPageState } from "./layout-page-store" + +type LayoutPageState = ReturnType + +export type PawworkProjectControlsInput = { + store: LayoutPageState + setStore: SetStoreFunction + language: { t: (key: string, params?: Record) => string } + projects: () => LocalProject[] + sessions: () => { directory: string }[] + renameProject: (project: LocalProject, next: string) => Promise + setWorkspaceName: (directory: string, next: string) => void +} + +export function createPawworkProjectControls(input: PawworkProjectControlsInput) { + function togglePinnedSession(sessionID: string) { + input.setStore("pawworkPinnedSessions", (current) => { + const next = current.filter((id) => id !== sessionID) + if (next.length !== current.length) return next + return [sessionID, ...current] + }) + } + + /** + * Cross-zone drag: All ⇄ Pinned with positional insert, or intra-Pinned + * reorder. `visiblePinnedIDs` is the rendered pinned order from the sidebar; + * `visibleTargetIndex` is a slot inside it. We translate to the raw + * pinned array so hidden / un-loaded pinned IDs keep their positions. + */ + function dragPawworkSession(args: { + sessionID: string + targetSection: "pinned" | "recent" + visiblePinnedIDs: string[] + visibleTargetIndex: number + }) { + input.setStore("pawworkPinnedSessions", (current) => { + if (args.targetSection === "recent") { + return unpinPawworkSession({ pinnedIDs: current, sourceID: args.sessionID }) + } + return reorderPawworkPinnedByVisible({ + pinnedIDs: current, + visiblePinnedIDs: args.visiblePinnedIDs, + sourceID: args.sessionID, + targetVisibleIndex: args.visibleTargetIndex, + }) + }) + } + + /** + * Menu-driven move up / down: keyboard-accessible reorder within the pinned + * zone. Operates on the visible pinned order so adjacency matches what the + * user sees; the helper reconciles back to the raw array. + */ + function movePinnedSessionByOne(args: { + sessionID: string + direction: "up" | "down" + visiblePinnedIDs: string[] + }) { + const visibleIndex = args.visiblePinnedIDs.indexOf(args.sessionID) + if (visibleIndex === -1) return + const offset = args.direction === "up" ? -1 : 1 + const nextVisibleIndex = Math.max(0, Math.min(args.visiblePinnedIDs.length - 1, visibleIndex + offset)) + if (nextVisibleIndex === visibleIndex) return + input.setStore("pawworkPinnedSessions", (current) => + reorderPawworkPinnedByVisible({ + pinnedIDs: current, + visiblePinnedIDs: args.visiblePinnedIDs, + sourceID: args.sessionID, + targetVisibleIndex: nextVisibleIndex, + }), + ) + } + + function setPawworkSortMode(mode: "time" | "project") { + input.setStore("pawworkSortMode", mode) + } + + function toggleProjectCollapsed(label: string) { + const current = input.store.pawworkProjectCollapsed + const next: Record = { ...current } + if (next[label]) delete next[label] + else next[label] = true + input.setStore("pawworkProjectCollapsed", reconcile(next)) + } + + function hideProject(projectKey: string) { + if (input.store.pawworkProjectHidden[projectKey]) return + input.setStore("pawworkProjectHidden", projectKey, true) + showToast({ + title: input.language.t("project.remove.toast.title"), + description: input.language.t("project.remove.toast.description"), + actions: [ + { + label: input.language.t("common.undo"), + onClick: () => unhideProject(projectKey), + }, + ], + }) + } + + function unhideProject(projectKey: string) { + if (!input.store.pawworkProjectHidden[projectKey]) return + input.setStore( + "pawworkProjectHidden", + produce((draft) => { + delete draft[projectKey] + }), + ) + } + + async function handleRenameProject(projectKey: string, next: string) { + const target = resolvePawworkProjectRenameTarget(projectKey, { + projects: input.projects(), + sessions: input.sessions(), + }) + if (!target) return + + if (target.type === "project") { + await input.renameProject(target.project, next) + return + } + + input.setWorkspaceName(target.directory, next) + } + + function expandPawworkProjectGroup(label: string | undefined) { + if (!label) return + if (!input.store.pawworkProjectCollapsed[label]) return + + const next: Record = { ...input.store.pawworkProjectCollapsed } + delete next[label] + input.setStore("pawworkProjectCollapsed", reconcile(next)) + } + + return { + togglePinnedSession, + dragPawworkSession, + movePinnedSessionByOne, + setPawworkSortMode, + toggleProjectCollapsed, + hideProject, + unhideProject, + handleRenameProject, + expandPawworkProjectGroup, + } +}