feat: improve layout manager (#290)
This commit is contained in:
@@ -298,9 +298,16 @@ function extractInitialScripts(
|
||||
return initialState?.repositoryScripts?.itemsByRepositoryId?.[repoId] ?? [];
|
||||
}
|
||||
|
||||
export default async function SessionPage({ params }: { params: Promise<{ sessionId: string }> }) {
|
||||
export default async function SessionPage({
|
||||
params,
|
||||
searchParams,
|
||||
}: {
|
||||
params: Promise<{ sessionId: string }>;
|
||||
searchParams: Promise<{ layout?: string }>;
|
||||
}) {
|
||||
let fetchedData: FetchedSessionData | null = null;
|
||||
const defaultLayouts = await readLayoutDefaults();
|
||||
const { layout: initialLayout } = await searchParams;
|
||||
|
||||
try {
|
||||
const { sessionId: paramSessionId } = await params;
|
||||
@@ -331,6 +338,7 @@ export default async function SessionPage({ params }: { params: Promise<{ sessio
|
||||
initialScripts={extractInitialScripts(initialState, task)}
|
||||
initialTerminals={initialTerminals}
|
||||
defaultLayouts={defaultLayouts}
|
||||
initialLayout={initialLayout}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -6,9 +6,9 @@ import type { StepType } from "@/components/task-create-dialog-types";
|
||||
import { selectPreferredBranch } from "@/lib/utils";
|
||||
import { getLocalStorage } from "@/lib/local-storage";
|
||||
import { STORAGE_KEYS } from "@/lib/settings/constants";
|
||||
import { useDockviewStore } from "@/lib/state/dockview-store";
|
||||
import { useContextFilesStore } from "@/lib/state/context-files-store";
|
||||
import { linkToSession } from "@/lib/links";
|
||||
import { INTENT_PLAN } from "@/lib/state/layout-manager";
|
||||
import { createTask } from "@/lib/api";
|
||||
|
||||
type CreateTaskParams = Parameters<typeof createTask>[0];
|
||||
@@ -76,10 +76,9 @@ export function activatePlanMode({
|
||||
router,
|
||||
}: ActivatePlanModeArgs) {
|
||||
setActiveDocument(sessionId, { type: "plan", taskId });
|
||||
useDockviewStore.getState().setLayoutIntent({ preset: "plan" });
|
||||
setPlanMode(sessionId, true);
|
||||
useContextFilesStore.getState().addFile(sessionId, { path: "plan:context", name: "Plan" });
|
||||
router.push(linkToSession(sessionId));
|
||||
router.push(linkToSession(sessionId, INTENT_PLAN));
|
||||
}
|
||||
|
||||
export type BuildCreatePayloadArgs = {
|
||||
|
||||
@@ -396,8 +396,12 @@ export function useChatPanelState({
|
||||
[planModeAvailable, rawHandlePlanModeChange, togglePlanLayout, planLayoutVisible],
|
||||
);
|
||||
|
||||
// Auto-disable plan mode if agent doesn't support MCP (e.g. started from create dialog)
|
||||
// Auto-disable plan mode if agent doesn't support MCP (e.g. started from create dialog).
|
||||
// Only clear state — do NOT call applyBuiltInPreset("default") because the layout
|
||||
// may have just been set via URL intent (?layout=plan) and we don't want to overwrite it.
|
||||
const hasAgentProfile = Boolean(sessionState.session?.agent_profile_id);
|
||||
const setPlanMode = useAppStore((s) => s.setPlanMode);
|
||||
const removeCtxFile = useContextFilesStore((s) => s.removeFile);
|
||||
const hasAutoDisabled = useRef(false);
|
||||
useEffect(() => {
|
||||
if (
|
||||
@@ -408,7 +412,8 @@ export function useChatPanelState({
|
||||
!hasAutoDisabled.current
|
||||
) {
|
||||
hasAutoDisabled.current = true;
|
||||
rawHandlePlanModeChange(false);
|
||||
setPlanMode(resolvedSessionId, false);
|
||||
removeCtxFile(resolvedSessionId, PLAN_CONTEXT_PATH);
|
||||
}
|
||||
if (!planModeEnabled) hasAutoDisabled.current = false;
|
||||
}, [
|
||||
@@ -416,7 +421,8 @@ export function useChatPanelState({
|
||||
hasAgentProfile,
|
||||
planModeAvailable,
|
||||
resolvedSessionId,
|
||||
rawHandlePlanModeChange,
|
||||
setPlanMode,
|
||||
removeCtxFile,
|
||||
]);
|
||||
|
||||
const contextFilesState = useContextFiles(resolvedSessionId);
|
||||
|
||||
@@ -627,11 +627,13 @@ type DockviewDesktopLayoutProps = {
|
||||
repository?: Repository | null;
|
||||
initialScripts?: RepositoryScript[];
|
||||
initialTerminals?: Terminal[];
|
||||
initialLayout?: string | null;
|
||||
};
|
||||
|
||||
export const DockviewDesktopLayout = memo(function DockviewDesktopLayout({
|
||||
sessionId,
|
||||
repository,
|
||||
initialLayout,
|
||||
}: DockviewDesktopLayoutProps) {
|
||||
const setApi = useDockviewStore((s) => s.setApi);
|
||||
const buildDefaultLayout = useDockviewStore((s) => s.buildDefaultLayout);
|
||||
@@ -672,9 +674,10 @@ export const DockviewDesktopLayout = memo(function DockviewDesktopLayout({
|
||||
setApi(api);
|
||||
|
||||
const currentSessionId = sessionIdRef.current;
|
||||
const restored = tryRestoreLayout(api, currentSessionId);
|
||||
// If a layout intent was passed via URL, skip saved layout restoration
|
||||
const restored = !initialLayout && tryRestoreLayout(api, currentSessionId);
|
||||
if (!restored) {
|
||||
buildDefaultLayout(api);
|
||||
buildDefaultLayout(api, initialLayout ?? undefined);
|
||||
}
|
||||
|
||||
useDockviewStore.setState({ currentLayoutSessionId: currentSessionId });
|
||||
@@ -693,7 +696,7 @@ export const DockviewDesktopLayout = memo(function DockviewDesktopLayout({
|
||||
setupLayoutPersistence(api, saveTimerRef, sessionIdRef);
|
||||
setupPortalCleanup(api);
|
||||
},
|
||||
[setApi, buildDefaultLayout],
|
||||
[setApi, buildDefaultLayout, initialLayout],
|
||||
);
|
||||
|
||||
// Release session-scoped portals + trigger layout switch on session change
|
||||
|
||||
@@ -8,7 +8,7 @@ import { TaskSwitcher } from "../task-switcher";
|
||||
import { WorkspaceSwitcher } from "../workspace-switcher";
|
||||
import { TaskCreateDialog } from "@/components/task-create-dialog";
|
||||
import { useAppStore, useAppStoreApi } from "@/components/state-provider";
|
||||
import { linkToSession } from "@/lib/links";
|
||||
import { replaceSessionUrl } from "@/lib/links";
|
||||
import { fetchWorkflowSnapshot, listWorkflows } from "@/lib/api";
|
||||
import { useTasks } from "@/hooks/use-tasks";
|
||||
import { useTaskActions } from "@/hooks/use-task-actions";
|
||||
@@ -173,19 +173,11 @@ type SheetNavOptions = {
|
||||
) => Promise<Array<{ id: string; updated_at?: string | null }>>;
|
||||
setActiveSession: (taskId: string, sessionId: string) => void;
|
||||
setActiveTask: (taskId: string) => void;
|
||||
updateUrl: (sessionId: string) => void;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
};
|
||||
|
||||
async function switchWorkspace(newWorkspaceId: string, opts: SheetNavOptions) {
|
||||
const {
|
||||
store,
|
||||
loadTaskSessionsForTask,
|
||||
setActiveSession,
|
||||
setActiveTask,
|
||||
updateUrl,
|
||||
onOpenChange,
|
||||
} = opts;
|
||||
const { store, loadTaskSessionsForTask, setActiveSession, setActiveTask, onOpenChange } = opts;
|
||||
store.setState((state) => ({ ...state, kanban: { ...state.kanban, isLoading: true } }));
|
||||
try {
|
||||
const workflowsResponse = await listWorkflows(newWorkspaceId, { cache: "no-store" });
|
||||
@@ -220,7 +212,7 @@ async function switchWorkspace(newWorkspaceId: string, opts: SheetNavOptions) {
|
||||
const mostRecentSession = sortByUpdatedAtDesc(sessions)[0];
|
||||
if (mostRecentSession) {
|
||||
setActiveSession(mostRecentTask.id, mostRecentSession.id);
|
||||
updateUrl(mostRecentSession.id);
|
||||
replaceSessionUrl(mostRecentSession.id);
|
||||
} else {
|
||||
setActiveTask(mostRecentTask.id);
|
||||
}
|
||||
@@ -233,7 +225,7 @@ async function switchWorkspace(newWorkspaceId: string, opts: SheetNavOptions) {
|
||||
}
|
||||
|
||||
function useWorkspaceAndTaskCreatedActions(opts: SheetNavOptions) {
|
||||
const { workspaceId, store, setActiveSession, setActiveTask, updateUrl, onOpenChange } = opts;
|
||||
const { workspaceId, store, setActiveSession, setActiveTask, onOpenChange } = opts;
|
||||
|
||||
const handleWorkspaceChange = useCallback(
|
||||
async (newWorkspaceId: string) => {
|
||||
@@ -278,11 +270,11 @@ function useWorkspaceAndTaskCreatedActions(opts: SheetNavOptions) {
|
||||
setActiveTask(task.id);
|
||||
if (meta?.taskSessionId) {
|
||||
setActiveSession(task.id, meta.taskSessionId);
|
||||
updateUrl(meta.taskSessionId);
|
||||
replaceSessionUrl(meta.taskSessionId);
|
||||
}
|
||||
onOpenChange(false);
|
||||
},
|
||||
[store, setActiveTask, setActiveSession, updateUrl, onOpenChange],
|
||||
[store, setActiveTask, setActiveSession, onOpenChange],
|
||||
);
|
||||
|
||||
return { handleWorkspaceChange, handleTaskCreated };
|
||||
@@ -296,18 +288,13 @@ function useSheetActions(workspaceId: string | null, onOpenChange: (open: boolea
|
||||
const { deleteTaskById, archiveTaskById } = useTaskActions();
|
||||
const { removeTaskFromBoard, loadTaskSessionsForTask } = useTaskRemoval({ store });
|
||||
|
||||
const updateUrl = useCallback((sessionId: string) => {
|
||||
if (typeof window === "undefined") return;
|
||||
window.history.replaceState({}, "", linkToSession(sessionId));
|
||||
}, []);
|
||||
|
||||
const handleSelectTask = useCallback(
|
||||
(taskId: string) => {
|
||||
const kanbanTasks = store.getState().kanban.tasks;
|
||||
const task = kanbanTasks.find((t) => t.id === taskId);
|
||||
if (task?.primarySessionId) {
|
||||
setActiveSession(taskId, task.primarySessionId);
|
||||
updateUrl(task.primarySessionId);
|
||||
replaceSessionUrl(task.primarySessionId);
|
||||
loadTaskSessionsForTask(taskId);
|
||||
onOpenChange(false);
|
||||
return;
|
||||
@@ -320,11 +307,11 @@ function useSheetActions(workspaceId: string | null, onOpenChange: (open: boolea
|
||||
return;
|
||||
}
|
||||
setActiveSession(taskId, sessionId);
|
||||
updateUrl(sessionId);
|
||||
replaceSessionUrl(sessionId);
|
||||
onOpenChange(false);
|
||||
});
|
||||
},
|
||||
[loadTaskSessionsForTask, setActiveSession, setActiveTask, updateUrl, store, onOpenChange],
|
||||
[loadTaskSessionsForTask, setActiveSession, setActiveTask, store, onOpenChange],
|
||||
);
|
||||
|
||||
const handleArchiveTask = useCallback(
|
||||
@@ -358,7 +345,6 @@ function useSheetActions(workspaceId: string | null, onOpenChange: (open: boolea
|
||||
loadTaskSessionsForTask,
|
||||
setActiveSession,
|
||||
setActiveTask,
|
||||
updateUrl,
|
||||
onOpenChange,
|
||||
});
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import { Badge } from "@kandev/ui/badge";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip";
|
||||
import { TaskCreateDialog } from "../task-create-dialog";
|
||||
import { useAppStore, useAppStoreApi } from "@/components/state-provider";
|
||||
import { linkToSession } from "@/lib/links";
|
||||
import { replaceSessionUrl } from "@/lib/links";
|
||||
import { useTaskSessions } from "@/hooks/use-task-sessions";
|
||||
import { performLayoutSwitch } from "@/lib/state/dockview-store";
|
||||
import type { TaskSession, TaskSessionState } from "@/lib/types/http";
|
||||
@@ -110,20 +110,16 @@ function useRunningSessionsClock(sessions: TaskSession[]) {
|
||||
function useSessionSelectionHandlers(taskId: string | null) {
|
||||
const setActiveSession = useAppStore((state) => state.setActiveSession);
|
||||
const appStore = useAppStoreApi();
|
||||
const updateUrl = useCallback((sessionId: string) => {
|
||||
if (typeof window === "undefined") return;
|
||||
window.history.replaceState({}, "", linkToSession(sessionId));
|
||||
}, []);
|
||||
const handleSelectSession = useCallback(
|
||||
(sessionId: string, close: () => void) => {
|
||||
if (!taskId) return;
|
||||
const oldSessionId = appStore.getState().tasks.activeSessionId;
|
||||
setActiveSession(taskId, sessionId);
|
||||
performLayoutSwitch(oldSessionId, sessionId);
|
||||
updateUrl(sessionId);
|
||||
replaceSessionUrl(sessionId);
|
||||
close();
|
||||
},
|
||||
[appStore, setActiveSession, taskId, updateUrl],
|
||||
[appStore, setActiveSession, taskId],
|
||||
);
|
||||
return { handleSelectSession };
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ type TaskLayoutProps = {
|
||||
remoteCreatedAt?: string | null;
|
||||
remoteCheckedAt?: string | null;
|
||||
remoteStatusError?: string | null;
|
||||
initialLayout?: string | null;
|
||||
};
|
||||
|
||||
export const TaskLayout = memo(function TaskLayout({
|
||||
@@ -55,6 +56,7 @@ export const TaskLayout = memo(function TaskLayout({
|
||||
remoteCreatedAt,
|
||||
remoteCheckedAt,
|
||||
remoteStatusError,
|
||||
initialLayout,
|
||||
}: TaskLayoutProps) {
|
||||
const { isMobile, isTablet } = useResponsiveBreakpoint();
|
||||
|
||||
@@ -101,6 +103,7 @@ export const TaskLayout = memo(function TaskLayout({
|
||||
repository={repository}
|
||||
initialScripts={initialScripts}
|
||||
initialTerminals={initialTerminals}
|
||||
initialLayout={initialLayout}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -35,6 +35,7 @@ type TaskPageContentProps = {
|
||||
initialScripts?: RepositoryScript[];
|
||||
initialTerminals?: Terminal[];
|
||||
defaultLayouts?: Record<string, Layout>;
|
||||
initialLayout?: string | null;
|
||||
};
|
||||
|
||||
function resolveEffectiveTask(
|
||||
@@ -192,6 +193,7 @@ type TaskPageInnerProps = {
|
||||
initialScripts: RepositoryScript[];
|
||||
initialTerminals?: Terminal[];
|
||||
defaultLayouts: Record<string, Layout>;
|
||||
initialLayout?: string | null;
|
||||
};
|
||||
|
||||
type RemoteExecutorStatus = {
|
||||
@@ -273,6 +275,7 @@ function buildTaskLayoutProps(params: {
|
||||
defaultLayouts: Record<string, Layout>;
|
||||
merged: ReturnType<typeof useMergedAgentState>;
|
||||
remote: ReturnType<typeof resolveRemoteExecutor>;
|
||||
initialLayout?: string | null;
|
||||
}) {
|
||||
const { taskProps, repository, effectiveSessionId, initialScripts, initialTerminals } = params;
|
||||
return {
|
||||
@@ -283,6 +286,7 @@ function buildTaskLayoutProps(params: {
|
||||
initialScripts,
|
||||
initialTerminals,
|
||||
defaultLayouts: params.defaultLayouts,
|
||||
initialLayout: params.initialLayout,
|
||||
taskTitle: taskProps.taskTitle,
|
||||
baseBranch: taskProps.baseBranch,
|
||||
worktreeBranch: params.merged.worktreeBranch,
|
||||
@@ -342,6 +346,7 @@ function TaskPageInner({
|
||||
initialScripts,
|
||||
initialTerminals,
|
||||
defaultLayouts,
|
||||
initialLayout,
|
||||
}: TaskPageInnerProps) {
|
||||
const taskProps = resolveTaskProps(task, repository);
|
||||
const remote = resolveRemoteExecutor(resumption.sessionStatus as RemoteExecutorStatus | null);
|
||||
@@ -374,6 +379,7 @@ function TaskPageInner({
|
||||
defaultLayouts,
|
||||
merged,
|
||||
remote,
|
||||
initialLayout,
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -493,6 +499,7 @@ export function TaskPageContent({
|
||||
initialScripts = [],
|
||||
initialTerminals,
|
||||
defaultLayouts = {},
|
||||
initialLayout,
|
||||
}: TaskPageContentProps) {
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
const [showDebugOverlay, setShowDebugOverlay] = useState(false);
|
||||
@@ -537,6 +544,7 @@ export function TaskPageContent({
|
||||
initialScripts={initialScripts}
|
||||
initialTerminals={initialTerminals}
|
||||
defaultLayouts={defaultLayouts}
|
||||
initialLayout={initialLayout}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { PanelRoot, PanelBody } from "./panel-primitives";
|
||||
import { IconPlus } from "@tabler/icons-react";
|
||||
import { TaskCreateDialog } from "@/components/task-create-dialog";
|
||||
import { useAppStore, useAppStoreApi } from "@/components/state-provider";
|
||||
import { linkToSession } from "@/lib/links";
|
||||
import { replaceSessionUrl } from "@/lib/links";
|
||||
import { useAllWorkflowSnapshots } from "@/hooks/domains/kanban/use-all-workflow-snapshots";
|
||||
import { useTaskActions } from "@/hooks/use-task-actions";
|
||||
import { useTaskRemoval } from "@/hooks/use-task-removal";
|
||||
@@ -229,18 +229,13 @@ function useSidebarActions(store: ReturnType<typeof useAppStoreApi>) {
|
||||
useLayoutSwitch: true,
|
||||
});
|
||||
|
||||
const updateUrl = useCallback((sessionId: string) => {
|
||||
if (typeof window === "undefined") return;
|
||||
window.history.replaceState({}, "", linkToSession(sessionId));
|
||||
}, []);
|
||||
|
||||
const switchToSession = useCallback(
|
||||
(taskId: string, sessionId: string, oldSessionId: string | null | undefined) => {
|
||||
setActiveSession(taskId, sessionId);
|
||||
performLayoutSwitch(oldSessionId ?? null, sessionId);
|
||||
updateUrl(sessionId);
|
||||
replaceSessionUrl(sessionId);
|
||||
},
|
||||
[setActiveSession, updateUrl],
|
||||
[setActiveSession],
|
||||
);
|
||||
|
||||
const handleSelectTask = useCallback(
|
||||
|
||||
@@ -4,9 +4,8 @@ import { useCallback } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { getWebSocketClient } from "@/lib/ws/connection";
|
||||
import { linkToSession } from "@/lib/links";
|
||||
import { INTENT_PR_REVIEW } from "@/lib/state/layout-manager";
|
||||
import { useAppStore } from "@/components/state-provider";
|
||||
import { useDockviewStore } from "@/lib/state/dockview-store";
|
||||
import { CENTER_GROUP, RIGHT_TOP_GROUP } from "@/lib/state/layout-manager";
|
||||
import type { Task } from "@/components/kanban-card";
|
||||
|
||||
type NavigationOptions = {
|
||||
@@ -55,23 +54,6 @@ async function preparePRSession(taskId: string): Promise<string | null> {
|
||||
}
|
||||
}
|
||||
|
||||
function setPRReviewLayoutIntent(): void {
|
||||
useDockviewStore.getState().setLayoutIntent({
|
||||
panels: [
|
||||
{
|
||||
id: "pr-detail",
|
||||
component: "pr-detail",
|
||||
title: "Pull Request",
|
||||
targetGroup: "center",
|
||||
},
|
||||
],
|
||||
activePanels: {
|
||||
[CENTER_GROUP]: "pr-detail",
|
||||
[RIGHT_TOP_GROUP]: "changes",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useKanbanNavigation({
|
||||
enablePreviewOnClick,
|
||||
isMobile,
|
||||
@@ -102,8 +84,7 @@ export function useKanbanNavigation({
|
||||
if (taskPRs[task.id]) {
|
||||
const sessionId = await preparePRSession(task.id);
|
||||
if (sessionId) {
|
||||
setPRReviewLayoutIntent();
|
||||
router.push(linkToSession(sessionId));
|
||||
router.push(linkToSession(sessionId, INTENT_PR_REVIEW));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
export function linkToSession(sessionId: string): string {
|
||||
return `/s/${sessionId}`;
|
||||
export function linkToSession(sessionId: string, layout?: string): string {
|
||||
const base = `/s/${sessionId}`;
|
||||
return layout ? `${base}?layout=${encodeURIComponent(layout)}` : base;
|
||||
}
|
||||
|
||||
/** Replace the browser URL to reflect the active session (no navigation). */
|
||||
export function replaceSessionUrl(sessionId: string): void {
|
||||
if (typeof window === "undefined") return;
|
||||
window.history.replaceState({}, "", linkToSession(sessionId));
|
||||
}
|
||||
|
||||
export function linkToTasks(workspaceId?: string): string {
|
||||
|
||||
@@ -15,8 +15,12 @@ import {
|
||||
defaultLayout,
|
||||
mergeCurrentPanelsIntoPreset,
|
||||
} from "./layout-manager";
|
||||
import type { BuiltInPreset, LayoutState, LayoutGroupIds, LayoutIntent } from "./layout-manager";
|
||||
import { injectIntentPanels, applyActivePanelOverrides } from "./layout-manager";
|
||||
import type { BuiltInPreset, LayoutState, LayoutGroupIds } from "./layout-manager";
|
||||
import {
|
||||
injectIntentPanels,
|
||||
applyActivePanelOverrides,
|
||||
resolveNamedIntent,
|
||||
} from "./layout-manager";
|
||||
import { buildFileStateActions } from "./dockview-file-state";
|
||||
import { buildPanelActions, buildExtraPanelActions } from "./dockview-panel-actions";
|
||||
|
||||
@@ -73,7 +77,7 @@ type DockviewStore = {
|
||||
updateFileState: (path: string, updates: Partial<FileEditorState>) => void;
|
||||
removeFileState: (path: string) => void;
|
||||
clearFileStates: () => void;
|
||||
buildDefaultLayout: (api: DockviewApi) => void;
|
||||
buildDefaultLayout: (api: DockviewApi, intentName?: string) => void;
|
||||
resetLayout: () => void;
|
||||
addChatPanel: () => void;
|
||||
addChangesPanel: (groupId?: string) => void;
|
||||
@@ -113,8 +117,6 @@ type DockviewStore = {
|
||||
setPinnedWidth: (columnId: string, width: number) => void;
|
||||
userDefaultLayout: LayoutState | null;
|
||||
setUserDefaultLayout: (layout: LayoutState | null) => void;
|
||||
layoutIntent: LayoutIntent | null;
|
||||
setLayoutIntent: (intent: LayoutIntent | null) => void;
|
||||
activeFilePath: string | null;
|
||||
pendingChatScrollTop: number | null;
|
||||
setPendingChatScrollTop: (value: number | null) => void;
|
||||
@@ -374,21 +376,27 @@ function buildPresetActions(set: StoreSet, get: StoreGet) {
|
||||
};
|
||||
}
|
||||
|
||||
function performBuildDefault(api: DockviewApi, set: StoreSet, get: StoreGet): void {
|
||||
const { userDefaultLayout, layoutIntent } = get();
|
||||
function performBuildDefault(
|
||||
api: DockviewApi,
|
||||
set: StoreSet,
|
||||
get: StoreGet,
|
||||
intentName?: string,
|
||||
): void {
|
||||
const { userDefaultLayout } = get();
|
||||
const intent = intentName ? resolveNamedIntent(intentName) : null;
|
||||
const freshPinned = new Map<string, number>();
|
||||
set({ isRestoringLayout: true, pinnedWidths: freshPinned, layoutIntent: null });
|
||||
set({ isRestoringLayout: true, pinnedWidths: freshPinned });
|
||||
|
||||
const basePreset = layoutIntent?.preset as BuiltInPreset | undefined;
|
||||
const basePreset = intent?.preset as BuiltInPreset | undefined;
|
||||
let state = basePreset
|
||||
? getPresetLayout(basePreset)
|
||||
: (userDefaultLayout ?? getPresetLayout("default"));
|
||||
|
||||
if (layoutIntent?.panels?.length) {
|
||||
state = injectIntentPanels(state, layoutIntent.panels);
|
||||
if (intent?.panels?.length) {
|
||||
state = injectIntentPanels(state, intent.panels);
|
||||
}
|
||||
if (layoutIntent?.activePanels) {
|
||||
state = applyActivePanelOverrides(state, layoutIntent.activePanels);
|
||||
if (intent?.activePanels) {
|
||||
state = applyActivePanelOverrides(state, intent.activePanels);
|
||||
}
|
||||
|
||||
const ids = applyLayout(api, state, freshPinned);
|
||||
@@ -441,8 +449,6 @@ export const useDockviewStore = create<DockviewStore>((set, get) => ({
|
||||
},
|
||||
userDefaultLayout: null,
|
||||
setUserDefaultLayout: (layout) => set({ userDefaultLayout: layout }),
|
||||
layoutIntent: null,
|
||||
setLayoutIntent: (intent) => set({ layoutIntent: intent }),
|
||||
...buildVisibilityActions(set, get),
|
||||
...buildPresetActions(set, get),
|
||||
isRestoringLayout: false,
|
||||
@@ -455,6 +461,19 @@ export const useDockviewStore = create<DockviewStore>((set, get) => ({
|
||||
switchSessionLayout: (oldSessionId, newSessionId) => {
|
||||
const { api, currentLayoutSessionId } = get();
|
||||
if (!api || currentLayoutSessionId === newSessionId) return;
|
||||
// When the first session becomes active (oldSessionId is null and no prior
|
||||
// layout session), onReady already built the correct layout (possibly with
|
||||
// an intent). Just adopt the current layout for the new session without
|
||||
// rebuilding, which would lose the intent-based layout.
|
||||
if (!oldSessionId && !currentLayoutSessionId) {
|
||||
set({ currentLayoutSessionId: newSessionId });
|
||||
try {
|
||||
setSessionLayout(newSessionId, api.toJSON());
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return;
|
||||
}
|
||||
const safeWidth = api.width;
|
||||
const safeHeight = api.height;
|
||||
set({ isRestoringLayout: true, currentLayoutSessionId: newSessionId });
|
||||
@@ -472,7 +491,7 @@ export const useDockviewStore = create<DockviewStore>((set, get) => ({
|
||||
set({ isRestoringLayout: false });
|
||||
}
|
||||
},
|
||||
buildDefaultLayout: (api) => performBuildDefault(api, set, get),
|
||||
buildDefaultLayout: (api, intentName) => performBuildDefault(api, set, get, intentName),
|
||||
resetLayout: () => {
|
||||
const { api } = get();
|
||||
if (api) get().buildDefaultLayout(api);
|
||||
|
||||
@@ -46,4 +46,10 @@ export type { LayoutGroupIds } from "./applier";
|
||||
export { mergeCurrentPanelsIntoPreset } from "./merger";
|
||||
|
||||
// Intent
|
||||
export { injectIntentPanels, applyActivePanelOverrides } from "./intent";
|
||||
export {
|
||||
INTENT_PLAN,
|
||||
INTENT_PR_REVIEW,
|
||||
injectIntentPanels,
|
||||
applyActivePanelOverrides,
|
||||
resolveNamedIntent,
|
||||
} from "./intent";
|
||||
|
||||
@@ -1,6 +1,34 @@
|
||||
import type { LayoutState, LayoutIntentPanel, LayoutPanel } from "./types";
|
||||
import type { LayoutState, LayoutIntent, LayoutIntentPanel, LayoutPanel } from "./types";
|
||||
import { CENTER_GROUP, RIGHT_TOP_GROUP, RIGHT_BOTTOM_GROUP } from "./constants";
|
||||
|
||||
/** Well-known intent name constants (used as URL ?layout= values). */
|
||||
export const INTENT_PLAN = "plan";
|
||||
export const INTENT_PR_REVIEW = "pr-review";
|
||||
|
||||
/** Registry of named layout intents, keyed by URL param value. */
|
||||
const NAMED_INTENTS: Record<string, LayoutIntent> = {
|
||||
[INTENT_PLAN]: { preset: "plan" },
|
||||
[INTENT_PR_REVIEW]: {
|
||||
panels: [
|
||||
{
|
||||
id: "pr-detail",
|
||||
component: "pr-detail",
|
||||
title: "Pull Request",
|
||||
targetGroup: "center",
|
||||
},
|
||||
],
|
||||
activePanels: {
|
||||
[CENTER_GROUP]: "pr-detail",
|
||||
[RIGHT_TOP_GROUP]: "changes",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/** Resolve a named intent string (from URL param) to a LayoutIntent, or null if unknown. */
|
||||
export function resolveNamedIntent(name: string): LayoutIntent | null {
|
||||
return NAMED_INTENTS[name] ?? null;
|
||||
}
|
||||
|
||||
/** Map short aliases to well-known group IDs. */
|
||||
const GROUP_ALIASES: Record<string, string> = {
|
||||
center: CENTER_GROUP,
|
||||
|
||||
Reference in New Issue
Block a user