Refactor WorkspacesLayout (#2052)
* init refactor * changes context * wip * logs context * workspaces layout context breakdown * sidebar context * move diffs to workspace context * compress workspaces layout * refactors * types * always show archived
This commit is contained in:
committed by
GitHub
parent
4071993561
commit
ea5954c8f5
@@ -149,6 +149,7 @@ fn generate_types_content() -> String {
|
||||
server::routes::task_attempts::workspace_summary::WorkspaceSummaryRequest::decl(),
|
||||
server::routes::task_attempts::workspace_summary::WorkspaceSummary::decl(),
|
||||
server::routes::task_attempts::workspace_summary::WorkspaceSummaryResponse::decl(),
|
||||
server::routes::task_attempts::workspace_summary::DiffStats::decl(),
|
||||
services::services::filesystem::DirectoryEntry::decl(),
|
||||
services::services::filesystem::DirectoryListResponse::decl(),
|
||||
services::services::file_search::SearchMode::decl(),
|
||||
|
||||
@@ -56,11 +56,11 @@ pub struct WorkspaceSummaryResponse {
|
||||
pub summaries: Vec<WorkspaceSummary>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
struct DiffStats {
|
||||
files_changed: usize,
|
||||
lines_added: usize,
|
||||
lines_removed: usize,
|
||||
#[derive(Debug, Clone, Default, Serialize, TS)]
|
||||
pub struct DiffStats {
|
||||
pub files_changed: usize,
|
||||
pub lines_added: usize,
|
||||
pub lines_removed: usize,
|
||||
}
|
||||
|
||||
/// Fetch summary information for workspaces filtered by archived status.
|
||||
|
||||
@@ -17,8 +17,8 @@ import {
|
||||
} from '@/stores/useUiPreferencesStore';
|
||||
import DisplayConversationEntry from '@/components/NormalizedConversation/DisplayConversationEntry';
|
||||
import { useMessageEditContext } from '@/contexts/MessageEditContext';
|
||||
import { useFileNavigation } from '@/contexts/FileNavigationContext';
|
||||
import { useLogNavigation } from '@/contexts/LogNavigationContext';
|
||||
import { useChangesView } from '@/contexts/ChangesViewContext';
|
||||
import { useLogsPanel } from '@/contexts/LogsPanelContext';
|
||||
import { useWorkspaceContext } from '@/contexts/WorkspaceContext';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
@@ -354,7 +354,7 @@ function FileEditEntry({
|
||||
expansionKey as PersistKey,
|
||||
pendingApproval
|
||||
);
|
||||
const { viewFileInChanges, diffPaths } = useFileNavigation();
|
||||
const { viewFileInChanges, diffPaths } = useChangesView();
|
||||
|
||||
// Calculate diff stats for edit changes
|
||||
const { additions, deletions } = useMemo(() => {
|
||||
@@ -561,7 +561,7 @@ function ToolSummaryEntry({
|
||||
`tool:${expansionKey}`,
|
||||
false
|
||||
);
|
||||
const { viewToolContentInPanel } = useLogNavigation();
|
||||
const { viewToolContentInPanel } = useLogsPanel();
|
||||
const textRef = useRef<HTMLSpanElement>(null);
|
||||
const [isTruncated, setIsTruncated] = useState(false);
|
||||
|
||||
|
||||
@@ -38,8 +38,11 @@ import {
|
||||
QuestionIcon,
|
||||
} from '@phosphor-icons/react';
|
||||
import { useDiffViewStore } from '@/stores/useDiffViewStore';
|
||||
import { useUiPreferencesStore } from '@/stores/useUiPreferencesStore';
|
||||
import { useLayoutStore } from '@/stores/useLayoutStore';
|
||||
import {
|
||||
useUiPreferencesStore,
|
||||
RIGHT_MAIN_PANEL_MODES,
|
||||
} from '@/stores/useUiPreferencesStore';
|
||||
|
||||
import { attemptsApi, tasksApi, repoApi } from '@/lib/api';
|
||||
import { attemptKeys } from '@/hooks/useAttempt';
|
||||
import { taskKeys } from '@/hooks/useTask';
|
||||
@@ -95,12 +98,12 @@ export interface ActionExecutorContext {
|
||||
// Context for evaluating action visibility and state conditions
|
||||
export interface ActionVisibilityContext {
|
||||
// Layout state
|
||||
isChangesMode: boolean;
|
||||
isLogsMode: boolean;
|
||||
isPreviewMode: boolean;
|
||||
isSidebarVisible: boolean;
|
||||
isMainPanelVisible: boolean;
|
||||
isGitPanelVisible: boolean;
|
||||
rightMainPanelMode:
|
||||
| (typeof RIGHT_MAIN_PANEL_MODES)[keyof typeof RIGHT_MAIN_PANEL_MODES]
|
||||
| null;
|
||||
isLeftSidebarVisible: boolean;
|
||||
isLeftMainPanelVisible: boolean;
|
||||
isRightSidebarVisible: boolean;
|
||||
isCreateMode: boolean;
|
||||
|
||||
// Workspace state
|
||||
@@ -415,7 +418,8 @@ export const Actions = {
|
||||
: 'Switch to Inline View',
|
||||
icon: ColumnsIcon,
|
||||
requiresTarget: false,
|
||||
isVisible: (ctx) => ctx.isChangesMode,
|
||||
isVisible: (ctx) =>
|
||||
ctx.rightMainPanelMode === RIGHT_MAIN_PANEL_MODES.CHANGES,
|
||||
isActive: (ctx) => ctx.diffViewMode === 'split',
|
||||
getIcon: (ctx) => (ctx.diffViewMode === 'split' ? ColumnsIcon : RowsIcon),
|
||||
getTooltip: (ctx) =>
|
||||
@@ -433,7 +437,8 @@ export const Actions = {
|
||||
: 'Ignore Whitespace Changes',
|
||||
icon: EyeSlashIcon,
|
||||
requiresTarget: false,
|
||||
isVisible: (ctx) => ctx.isChangesMode,
|
||||
isVisible: (ctx) =>
|
||||
ctx.rightMainPanelMode === RIGHT_MAIN_PANEL_MODES.CHANGES,
|
||||
execute: () => {
|
||||
const store = useDiffViewStore.getState();
|
||||
store.setIgnoreWhitespace(!store.ignoreWhitespace);
|
||||
@@ -448,7 +453,8 @@ export const Actions = {
|
||||
: 'Enable Line Wrapping',
|
||||
icon: TextAlignLeftIcon,
|
||||
requiresTarget: false,
|
||||
isVisible: (ctx) => ctx.isChangesMode,
|
||||
isVisible: (ctx) =>
|
||||
ctx.rightMainPanelMode === RIGHT_MAIN_PANEL_MODES.CHANGES,
|
||||
execute: () => {
|
||||
const store = useDiffViewStore.getState();
|
||||
store.setWrapText(!store.wrapText);
|
||||
@@ -456,94 +462,106 @@ export const Actions = {
|
||||
},
|
||||
|
||||
// === Layout Panel Actions ===
|
||||
ToggleSidebar: {
|
||||
id: 'toggle-sidebar',
|
||||
ToggleLeftSidebar: {
|
||||
id: 'toggle-left-sidebar',
|
||||
label: () =>
|
||||
useLayoutStore.getState().isSidebarVisible
|
||||
? 'Hide Sidebar'
|
||||
: 'Show Sidebar',
|
||||
useUiPreferencesStore.getState().isLeftSidebarVisible
|
||||
? 'Hide Left Sidebar'
|
||||
: 'Show Left Sidebar',
|
||||
icon: SidebarSimpleIcon,
|
||||
requiresTarget: false,
|
||||
isActive: (ctx) => ctx.isSidebarVisible,
|
||||
isActive: (ctx) => ctx.isLeftSidebarVisible,
|
||||
execute: () => {
|
||||
useLayoutStore.getState().toggleSidebar();
|
||||
useUiPreferencesStore.getState().toggleLeftSidebar();
|
||||
},
|
||||
},
|
||||
|
||||
ToggleMainPanel: {
|
||||
id: 'toggle-main-panel',
|
||||
ToggleLeftMainPanel: {
|
||||
id: 'toggle-left-main-panel',
|
||||
label: () =>
|
||||
useLayoutStore.getState().isMainPanelVisible
|
||||
useUiPreferencesStore.getState().isLeftMainPanelVisible
|
||||
? 'Hide Chat Panel'
|
||||
: 'Show Chat Panel',
|
||||
icon: ChatsTeardropIcon,
|
||||
requiresTarget: false,
|
||||
isActive: (ctx) => ctx.isMainPanelVisible,
|
||||
isEnabled: (ctx) => !(ctx.isMainPanelVisible && !ctx.isChangesMode),
|
||||
isActive: (ctx) => ctx.isLeftMainPanelVisible,
|
||||
isEnabled: (ctx) =>
|
||||
!(ctx.isLeftMainPanelVisible && ctx.rightMainPanelMode === null),
|
||||
execute: () => {
|
||||
useLayoutStore.getState().toggleMainPanel();
|
||||
useUiPreferencesStore.getState().toggleLeftMainPanel();
|
||||
},
|
||||
},
|
||||
|
||||
ToggleGitPanel: {
|
||||
id: 'toggle-git-panel',
|
||||
ToggleRightSidebar: {
|
||||
id: 'toggle-right-sidebar',
|
||||
label: () =>
|
||||
useLayoutStore.getState().isGitPanelVisible
|
||||
? 'Hide Git Panel'
|
||||
: 'Show Git Panel',
|
||||
useUiPreferencesStore.getState().isRightSidebarVisible
|
||||
? 'Hide Right Sidebar'
|
||||
: 'Show Right Sidebar',
|
||||
icon: RightSidebarIcon,
|
||||
requiresTarget: false,
|
||||
isActive: (ctx) => ctx.isGitPanelVisible,
|
||||
isActive: (ctx) => ctx.isRightSidebarVisible,
|
||||
execute: () => {
|
||||
useLayoutStore.getState().toggleGitPanel();
|
||||
useUiPreferencesStore.getState().toggleRightSidebar();
|
||||
},
|
||||
},
|
||||
|
||||
ToggleChangesMode: {
|
||||
id: 'toggle-changes-mode',
|
||||
label: () =>
|
||||
useLayoutStore.getState().isChangesMode
|
||||
useUiPreferencesStore.getState().rightMainPanelMode ===
|
||||
RIGHT_MAIN_PANEL_MODES.CHANGES
|
||||
? 'Hide Changes Panel'
|
||||
: 'Show Changes Panel',
|
||||
icon: GitDiffIcon,
|
||||
requiresTarget: false,
|
||||
isVisible: (ctx) => !ctx.isCreateMode,
|
||||
isActive: (ctx) => ctx.isChangesMode,
|
||||
isActive: (ctx) =>
|
||||
ctx.rightMainPanelMode === RIGHT_MAIN_PANEL_MODES.CHANGES,
|
||||
isEnabled: (ctx) => !ctx.isCreateMode,
|
||||
execute: () => {
|
||||
useLayoutStore.getState().toggleChangesMode();
|
||||
useUiPreferencesStore
|
||||
.getState()
|
||||
.toggleRightMainPanelMode(RIGHT_MAIN_PANEL_MODES.CHANGES);
|
||||
},
|
||||
},
|
||||
|
||||
ToggleLogsMode: {
|
||||
id: 'toggle-logs-mode',
|
||||
label: () =>
|
||||
useLayoutStore.getState().isLogsMode
|
||||
useUiPreferencesStore.getState().rightMainPanelMode ===
|
||||
RIGHT_MAIN_PANEL_MODES.LOGS
|
||||
? 'Hide Logs Panel'
|
||||
: 'Show Logs Panel',
|
||||
icon: TerminalIcon,
|
||||
requiresTarget: false,
|
||||
isVisible: (ctx) => !ctx.isCreateMode,
|
||||
isActive: (ctx) => ctx.isLogsMode,
|
||||
isActive: (ctx) => ctx.rightMainPanelMode === RIGHT_MAIN_PANEL_MODES.LOGS,
|
||||
isEnabled: (ctx) => !ctx.isCreateMode,
|
||||
execute: () => {
|
||||
useLayoutStore.getState().toggleLogsMode();
|
||||
useUiPreferencesStore
|
||||
.getState()
|
||||
.toggleRightMainPanelMode(RIGHT_MAIN_PANEL_MODES.LOGS);
|
||||
},
|
||||
},
|
||||
|
||||
TogglePreviewMode: {
|
||||
id: 'toggle-preview-mode',
|
||||
label: () =>
|
||||
useLayoutStore.getState().isPreviewMode
|
||||
useUiPreferencesStore.getState().rightMainPanelMode ===
|
||||
RIGHT_MAIN_PANEL_MODES.PREVIEW
|
||||
? 'Hide Preview Panel'
|
||||
: 'Show Preview Panel',
|
||||
icon: DesktopIcon,
|
||||
requiresTarget: false,
|
||||
isVisible: (ctx) => !ctx.isCreateMode,
|
||||
isActive: (ctx) => ctx.isPreviewMode,
|
||||
isActive: (ctx) =>
|
||||
ctx.rightMainPanelMode === RIGHT_MAIN_PANEL_MODES.PREVIEW,
|
||||
isEnabled: (ctx) => !ctx.isCreateMode,
|
||||
execute: () => {
|
||||
useLayoutStore.getState().togglePreviewMode();
|
||||
useUiPreferencesStore
|
||||
.getState()
|
||||
.toggleRightMainPanelMode(RIGHT_MAIN_PANEL_MODES.PREVIEW);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -592,7 +610,8 @@ export const Actions = {
|
||||
},
|
||||
icon: CaretDoubleUpIcon,
|
||||
requiresTarget: false,
|
||||
isVisible: (ctx) => ctx.isChangesMode,
|
||||
isVisible: (ctx) =>
|
||||
ctx.rightMainPanelMode === RIGHT_MAIN_PANEL_MODES.CHANGES,
|
||||
getIcon: (ctx) =>
|
||||
ctx.isAllDiffsExpanded ? CaretDoubleUpIcon : CaretDoubleDownIcon,
|
||||
getTooltip: (ctx) =>
|
||||
@@ -686,7 +705,9 @@ export const Actions = {
|
||||
} else {
|
||||
ctx.startDevServer();
|
||||
// Auto-open preview mode when starting dev server
|
||||
useLayoutStore.getState().setPreviewMode(true);
|
||||
useUiPreferencesStore
|
||||
.getState()
|
||||
.setRightMainPanelMode(RIGHT_MAIN_PANEL_MODES.PREVIEW);
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -949,12 +970,12 @@ export const NavbarActionGroups = {
|
||||
Actions.ToggleDiffViewMode,
|
||||
Actions.ToggleAllDiffs,
|
||||
NavbarDivider,
|
||||
Actions.ToggleSidebar,
|
||||
Actions.ToggleMainPanel,
|
||||
Actions.ToggleLeftSidebar,
|
||||
Actions.ToggleLeftMainPanel,
|
||||
Actions.ToggleChangesMode,
|
||||
Actions.ToggleLogsMode,
|
||||
Actions.TogglePreviewMode,
|
||||
Actions.ToggleGitPanel,
|
||||
Actions.ToggleRightSidebar,
|
||||
NavbarDivider,
|
||||
Actions.OpenCommandBar,
|
||||
Actions.Feedback,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Icon } from '@phosphor-icons/react';
|
||||
import { type ActionDefinition, type ActionVisibilityContext } from './index';
|
||||
import { Actions } from './index';
|
||||
import { RIGHT_MAIN_PANEL_MODES } from '@/stores/useUiPreferencesStore';
|
||||
|
||||
// Define page IDs first to avoid circular reference
|
||||
export type PageId =
|
||||
@@ -130,7 +131,8 @@ export const Pages: Record<StaticPageId, CommandBarPage> = {
|
||||
id: 'diff-options',
|
||||
title: 'Diff Options',
|
||||
parent: 'root',
|
||||
isVisible: (ctx) => ctx.isChangesMode,
|
||||
isVisible: (ctx) =>
|
||||
ctx.rightMainPanelMode === RIGHT_MAIN_PANEL_MODES.CHANGES,
|
||||
items: [
|
||||
{
|
||||
type: 'group',
|
||||
@@ -155,9 +157,9 @@ export const Pages: Record<StaticPageId, CommandBarPage> = {
|
||||
type: 'group',
|
||||
label: 'Panels',
|
||||
items: [
|
||||
{ type: 'action', action: Actions.ToggleSidebar },
|
||||
{ type: 'action', action: Actions.ToggleMainPanel },
|
||||
{ type: 'action', action: Actions.ToggleGitPanel },
|
||||
{ type: 'action', action: Actions.ToggleLeftSidebar },
|
||||
{ type: 'action', action: Actions.ToggleLeftMainPanel },
|
||||
{ type: 'action', action: Actions.ToggleRightSidebar },
|
||||
{ type: 'action', action: Actions.ToggleChangesMode },
|
||||
{ type: 'action', action: Actions.ToggleLogsMode },
|
||||
{ type: 'action', action: Actions.TogglePreviewMode },
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useLayoutStore } from '@/stores/useLayoutStore';
|
||||
import { useDiffViewStore, useDiffViewMode } from '@/stores/useDiffViewStore';
|
||||
import { useUiPreferencesStore } from '@/stores/useUiPreferencesStore';
|
||||
import { useDiffViewStore, useDiffViewMode } from '@/stores/useDiffViewStore';
|
||||
import { useWorkspaceContext } from '@/contexts/WorkspaceContext';
|
||||
import { useUserSystem } from '@/components/ConfigProvider';
|
||||
import { useDevServer } from '@/hooks/useDevServer';
|
||||
@@ -23,7 +22,7 @@ import type { CommandBarPage } from './pages';
|
||||
* action visibility and state conditions.
|
||||
*/
|
||||
export function useActionVisibilityContext(): ActionVisibilityContext {
|
||||
const layout = useLayoutStore();
|
||||
const layout = useUiPreferencesStore();
|
||||
const { workspace, workspaceId, isCreateMode, repos } = useWorkspaceContext();
|
||||
const diffPaths = useDiffViewStore((s) => s.diffPaths);
|
||||
const diffViewMode = useDiffViewMode();
|
||||
@@ -62,12 +61,10 @@ export function useActionVisibilityContext(): ActionVisibilityContext {
|
||||
false;
|
||||
|
||||
return {
|
||||
isChangesMode: layout.isChangesMode,
|
||||
isLogsMode: layout.isLogsMode,
|
||||
isPreviewMode: layout.isPreviewMode,
|
||||
isSidebarVisible: layout.isSidebarVisible,
|
||||
isMainPanelVisible: layout.isMainPanelVisible,
|
||||
isGitPanelVisible: layout.isGitPanelVisible,
|
||||
rightMainPanelMode: layout.rightMainPanelMode,
|
||||
isLeftSidebarVisible: layout.isLeftSidebarVisible,
|
||||
isLeftMainPanelVisible: layout.isLeftMainPanelVisible,
|
||||
isRightSidebarVisible: layout.isRightSidebarVisible,
|
||||
isCreateMode,
|
||||
hasWorkspace: !!workspace,
|
||||
workspaceArchived: workspace?.archived ?? false,
|
||||
@@ -84,12 +81,10 @@ export function useActionVisibilityContext(): ActionVisibilityContext {
|
||||
isAttemptRunning: isAttemptRunningVisible,
|
||||
};
|
||||
}, [
|
||||
layout.isChangesMode,
|
||||
layout.isLogsMode,
|
||||
layout.isPreviewMode,
|
||||
layout.isSidebarVisible,
|
||||
layout.isMainPanelVisible,
|
||||
layout.isGitPanelVisible,
|
||||
layout.rightMainPanelMode,
|
||||
layout.isLeftSidebarVisible,
|
||||
layout.isLeftMainPanelVisible,
|
||||
layout.isRightSidebarVisible,
|
||||
isCreateMode,
|
||||
workspace,
|
||||
repos,
|
||||
|
||||
@@ -8,6 +8,9 @@ import {
|
||||
} from 'react';
|
||||
import { ChangesPanel } from '../views/ChangesPanel';
|
||||
import { sortDiffs } from '@/utils/fileTreeUtils';
|
||||
import { useChangesView } from '@/contexts/ChangesViewContext';
|
||||
import { useWorkspaceContext } from '@/contexts/WorkspaceContext';
|
||||
import { useTask } from '@/hooks/useTask';
|
||||
import type { Diff, DiffChangeKind } from 'shared/types';
|
||||
|
||||
// Auto-collapse defaults based on change type (matches DiffsPanel behavior)
|
||||
@@ -128,24 +131,20 @@ function useInViewObserver(
|
||||
}
|
||||
|
||||
interface ChangesPanelContainerProps {
|
||||
diffs: Diff[];
|
||||
selectedFilePath?: string | null;
|
||||
onFileInViewChange?: (path: string) => void;
|
||||
className?: string;
|
||||
/** Project ID for @ mentions in comments */
|
||||
projectId?: string;
|
||||
/** Attempt ID for opening files in IDE */
|
||||
attemptId?: string;
|
||||
}
|
||||
|
||||
export function ChangesPanelContainer({
|
||||
diffs,
|
||||
selectedFilePath,
|
||||
onFileInViewChange,
|
||||
className,
|
||||
projectId,
|
||||
attemptId,
|
||||
}: ChangesPanelContainerProps) {
|
||||
const { diffs, workspace } = useWorkspaceContext();
|
||||
const { data: task } = useTask(workspace?.task_id, {
|
||||
enabled: !!workspace?.task_id,
|
||||
});
|
||||
const { selectedFilePath, setFileInView } = useChangesView();
|
||||
const diffRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
// Track which diffs we've processed for auto-collapse
|
||||
@@ -155,7 +154,7 @@ export function ChangesPanelContainer({
|
||||
const observeElement = useInViewObserver(
|
||||
diffRefs,
|
||||
containerRef,
|
||||
onFileInViewChange
|
||||
setFileInView
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -208,7 +207,7 @@ export function ChangesPanelContainer({
|
||||
className={className}
|
||||
diffItems={diffItems}
|
||||
onDiffRef={handleDiffRef}
|
||||
projectId={projectId}
|
||||
projectId={task?.project_id}
|
||||
attemptId={attemptId}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -8,12 +8,12 @@ import {
|
||||
} from '@/utils/fileTreeUtils';
|
||||
import { usePersistedCollapsedPaths } from '@/stores/useUiPreferencesStore';
|
||||
import { useWorkspaceContext } from '@/contexts/WorkspaceContext';
|
||||
import { useChangesView } from '@/contexts/ChangesViewContext';
|
||||
import type { Diff } from 'shared/types';
|
||||
|
||||
interface FileTreeContainerProps {
|
||||
workspaceId?: string;
|
||||
diffs: Diff[];
|
||||
selectedFilePath?: string | null;
|
||||
onSelectFile?: (path: string, diff: Diff) => void;
|
||||
className?: string;
|
||||
}
|
||||
@@ -21,10 +21,10 @@ interface FileTreeContainerProps {
|
||||
export function FileTreeContainer({
|
||||
workspaceId,
|
||||
diffs,
|
||||
selectedFilePath,
|
||||
onSelectFile,
|
||||
className,
|
||||
}: FileTreeContainerProps) {
|
||||
const { fileInView } = useChangesView();
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [collapsedPaths, setCollapsedPaths] =
|
||||
usePersistedCollapsedPaths(workspaceId);
|
||||
@@ -39,19 +39,19 @@ export function FileTreeContainer({
|
||||
isGitHubCommentsLoading,
|
||||
} = useWorkspaceContext();
|
||||
|
||||
// Sync selectedPath with external selectedFilePath prop and scroll into view
|
||||
// Sync selectedPath with fileInView from context and scroll into view
|
||||
useEffect(() => {
|
||||
if (selectedFilePath !== undefined) {
|
||||
setSelectedPath(selectedFilePath);
|
||||
if (fileInView !== undefined) {
|
||||
setSelectedPath(fileInView);
|
||||
// Scroll the selected node into view if needed
|
||||
if (selectedFilePath) {
|
||||
const el = nodeRefs.current.get(selectedFilePath);
|
||||
if (fileInView) {
|
||||
const el = nodeRefs.current.get(fileInView);
|
||||
if (el) {
|
||||
el.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [selectedFilePath]);
|
||||
}, [fileInView]);
|
||||
|
||||
const handleNodeRef = useCallback(
|
||||
(path: string, el: HTMLDivElement | null) => {
|
||||
|
||||
288
frontend/src/components/ui-new/containers/GitPanelContainer.tsx
Normal file
288
frontend/src/components/ui-new/containers/GitPanelContainer.tsx
Normal file
@@ -0,0 +1,288 @@
|
||||
import { useState, useCallback, useMemo, useEffect, useRef } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useActions } from '@/contexts/ActionsContext';
|
||||
import { usePush } from '@/hooks/usePush';
|
||||
import { useRenameBranch } from '@/hooks/useRenameBranch';
|
||||
import { useBranchStatus } from '@/hooks/useBranchStatus';
|
||||
import { repoApi } from '@/lib/api';
|
||||
import { ConfirmDialog } from '@/components/ui-new/dialogs/ConfirmDialog';
|
||||
import { ForcePushDialog } from '@/components/dialogs/git/ForcePushDialog';
|
||||
import { GitPanel, type RepoInfo } from '@/components/ui-new/views/GitPanel';
|
||||
import { Actions } from '@/components/ui-new/actions';
|
||||
import type { RepoAction } from '@/components/ui-new/primitives/RepoCard';
|
||||
import type {
|
||||
Workspace,
|
||||
RepoWithTargetBranch,
|
||||
Diff,
|
||||
Merge,
|
||||
} from 'shared/types';
|
||||
|
||||
export interface GitPanelContainerProps {
|
||||
selectedWorkspace: Workspace | undefined;
|
||||
repos: RepoWithTargetBranch[];
|
||||
diffs: Diff[];
|
||||
}
|
||||
|
||||
type PushState = 'idle' | 'pending' | 'success' | 'error';
|
||||
|
||||
export function GitPanelContainer({
|
||||
selectedWorkspace,
|
||||
repos,
|
||||
diffs,
|
||||
}: GitPanelContainerProps) {
|
||||
const { executeAction } = useActions();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Hooks for branch management (moved from WorkspacesLayout)
|
||||
const renameBranch = useRenameBranch(selectedWorkspace?.id);
|
||||
const { data: branchStatus } = useBranchStatus(selectedWorkspace?.id);
|
||||
|
||||
const handleBranchNameChange = useCallback(
|
||||
(newName: string) => {
|
||||
renameBranch.mutate(newName);
|
||||
},
|
||||
[renameBranch]
|
||||
);
|
||||
|
||||
// Transform repos to RepoInfo format (moved from WorkspacesLayout)
|
||||
const repoInfos: RepoInfo[] = useMemo(
|
||||
() =>
|
||||
repos.map((repo) => {
|
||||
const repoStatus = branchStatus?.find((s) => s.repo_id === repo.id);
|
||||
|
||||
let prNumber: number | undefined;
|
||||
let prUrl: string | undefined;
|
||||
let prStatus: 'open' | 'merged' | 'closed' | 'unknown' | undefined;
|
||||
|
||||
if (repoStatus?.merges) {
|
||||
const openPR = repoStatus.merges.find(
|
||||
(m: Merge) => m.type === 'pr' && m.pr_info.status === 'open'
|
||||
);
|
||||
const mergedPR = repoStatus.merges.find(
|
||||
(m: Merge) => m.type === 'pr' && m.pr_info.status === 'merged'
|
||||
);
|
||||
|
||||
const relevantPR = openPR || mergedPR;
|
||||
if (relevantPR && relevantPR.type === 'pr') {
|
||||
prNumber = Number(relevantPR.pr_info.number);
|
||||
prUrl = relevantPR.pr_info.url;
|
||||
prStatus = relevantPR.pr_info.status;
|
||||
}
|
||||
}
|
||||
|
||||
const repoDiffs = diffs.filter((d) => d.repoId === repo.id);
|
||||
const filesChanged = repoDiffs.length;
|
||||
const linesAdded = repoDiffs.reduce(
|
||||
(sum, d) => sum + (d.additions ?? 0),
|
||||
0
|
||||
);
|
||||
const linesRemoved = repoDiffs.reduce(
|
||||
(sum, d) => sum + (d.deletions ?? 0),
|
||||
0
|
||||
);
|
||||
|
||||
return {
|
||||
id: repo.id,
|
||||
name: repo.display_name || repo.name,
|
||||
targetBranch: repo.target_branch || 'main',
|
||||
commitsAhead: repoStatus?.commits_ahead ?? 0,
|
||||
remoteCommitsAhead: repoStatus?.remote_commits_ahead ?? 0,
|
||||
filesChanged,
|
||||
linesAdded,
|
||||
linesRemoved,
|
||||
prNumber,
|
||||
prUrl,
|
||||
prStatus,
|
||||
};
|
||||
}),
|
||||
[repos, diffs, branchStatus]
|
||||
);
|
||||
|
||||
// Track push state per repo: idle, pending, success, or error
|
||||
const [pushStates, setPushStates] = useState<Record<string, PushState>>({});
|
||||
const pushStatesRef = useRef<Record<string, PushState>>({});
|
||||
pushStatesRef.current = pushStates;
|
||||
const successTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const currentPushRepoRef = useRef<string | null>(null);
|
||||
|
||||
// Reset push-related state when the selected workspace changes to avoid
|
||||
// leaking push state across workspaces with repos that share the same ID.
|
||||
useEffect(() => {
|
||||
setPushStates({});
|
||||
pushStatesRef.current = {};
|
||||
currentPushRepoRef.current = null;
|
||||
|
||||
if (successTimeoutRef.current) {
|
||||
clearTimeout(successTimeoutRef.current);
|
||||
successTimeoutRef.current = null;
|
||||
}
|
||||
}, [selectedWorkspace?.id]);
|
||||
// Use push hook for direct API access with proper error handling
|
||||
const pushMutation = usePush(
|
||||
selectedWorkspace?.id,
|
||||
// onSuccess
|
||||
() => {
|
||||
const repoId = currentPushRepoRef.current;
|
||||
if (!repoId) return;
|
||||
setPushStates((prev) => ({ ...prev, [repoId]: 'success' }));
|
||||
// Clear success state after 2 seconds
|
||||
successTimeoutRef.current = setTimeout(() => {
|
||||
setPushStates((prev) => ({ ...prev, [repoId]: 'idle' }));
|
||||
}, 2000);
|
||||
},
|
||||
// onError
|
||||
async (err, errorData) => {
|
||||
const repoId = currentPushRepoRef.current;
|
||||
if (!repoId) return;
|
||||
|
||||
// Handle force push required - show confirmation dialog
|
||||
if (errorData?.type === 'force_push_required' && selectedWorkspace?.id) {
|
||||
setPushStates((prev) => ({ ...prev, [repoId]: 'idle' }));
|
||||
await ForcePushDialog.show({
|
||||
attemptId: selectedWorkspace.id,
|
||||
repoId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Show error state and dialog for other errors
|
||||
setPushStates((prev) => ({ ...prev, [repoId]: 'error' }));
|
||||
const message =
|
||||
err instanceof Error ? err.message : 'Failed to push changes';
|
||||
ConfirmDialog.show({
|
||||
title: 'Error',
|
||||
message,
|
||||
confirmText: 'OK',
|
||||
showCancelButton: false,
|
||||
variant: 'destructive',
|
||||
});
|
||||
// Clear error state after 3 seconds
|
||||
successTimeoutRef.current = setTimeout(() => {
|
||||
setPushStates((prev) => ({ ...prev, [repoId]: 'idle' }));
|
||||
}, 3000);
|
||||
}
|
||||
);
|
||||
|
||||
// Clean up timeout on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (successTimeoutRef.current) {
|
||||
clearTimeout(successTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Compute repoInfos with push button state
|
||||
const repoInfosWithPushButton = useMemo(
|
||||
() =>
|
||||
repoInfos.map((repo) => {
|
||||
const state = pushStates[repo.id] ?? 'idle';
|
||||
const hasUnpushedCommits =
|
||||
repo.prStatus === 'open' && (repo.remoteCommitsAhead ?? 0) > 0;
|
||||
// Show push button if there are unpushed commits OR if we're in a push flow
|
||||
// (pending/success/error states keep the button visible for feedback)
|
||||
const isInPushFlow = state !== 'idle';
|
||||
return {
|
||||
...repo,
|
||||
showPushButton: hasUnpushedCommits && !isInPushFlow,
|
||||
isPushPending: state === 'pending',
|
||||
isPushSuccess: state === 'success',
|
||||
isPushError: state === 'error',
|
||||
};
|
||||
}),
|
||||
[repoInfos, pushStates]
|
||||
);
|
||||
|
||||
// Handle copying repo path to clipboard
|
||||
const handleCopyPath = useCallback(
|
||||
(repoId: string) => {
|
||||
const repo = repos.find((r) => r.id === repoId);
|
||||
if (repo?.path) {
|
||||
navigator.clipboard.writeText(repo.path);
|
||||
}
|
||||
},
|
||||
[repos]
|
||||
);
|
||||
|
||||
// Handle opening repo in editor
|
||||
const handleOpenInEditor = useCallback(async (repoId: string) => {
|
||||
try {
|
||||
const response = await repoApi.openEditor(repoId, {
|
||||
editor_type: null,
|
||||
file_path: null,
|
||||
});
|
||||
|
||||
// If a URL is returned (remote mode), open it in a new tab
|
||||
if (response.url) {
|
||||
window.open(response.url, '_blank');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to open repo in editor:', err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Handle GitPanel actions using the action system
|
||||
const handleActionsClick = useCallback(
|
||||
async (repoId: string, action: RepoAction) => {
|
||||
if (!selectedWorkspace?.id) return;
|
||||
|
||||
// Map RepoAction to Action definitions
|
||||
const actionMap = {
|
||||
'pull-request': Actions.GitCreatePR,
|
||||
merge: Actions.GitMerge,
|
||||
rebase: Actions.GitRebase,
|
||||
'change-target': Actions.GitChangeTarget,
|
||||
push: Actions.GitPush,
|
||||
};
|
||||
|
||||
const actionDef = actionMap[action];
|
||||
if (!actionDef) return;
|
||||
|
||||
// Execute git action with workspaceId and repoId
|
||||
await executeAction(actionDef, selectedWorkspace.id, repoId);
|
||||
},
|
||||
[selectedWorkspace, executeAction]
|
||||
);
|
||||
|
||||
// Handle push button click - use mutation for proper state tracking
|
||||
const handlePushClick = useCallback(
|
||||
(repoId: string) => {
|
||||
// Use ref to check current state to avoid stale closure
|
||||
if (pushStatesRef.current[repoId] === 'pending') return;
|
||||
|
||||
// Clear any existing timeout
|
||||
if (successTimeoutRef.current) {
|
||||
clearTimeout(successTimeoutRef.current);
|
||||
successTimeoutRef.current = null;
|
||||
}
|
||||
|
||||
// Track which repo we're pushing
|
||||
currentPushRepoRef.current = repoId;
|
||||
setPushStates((prev) => ({ ...prev, [repoId]: 'pending' }));
|
||||
pushMutation.mutate({ repo_id: repoId });
|
||||
},
|
||||
[pushMutation]
|
||||
);
|
||||
|
||||
// Handle opening repository settings
|
||||
const handleOpenSettings = useCallback(
|
||||
(repoId: string) => {
|
||||
navigate(`/settings/repos?repoId=${repoId}`);
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
|
||||
return (
|
||||
<GitPanel
|
||||
repos={repoInfosWithPushButton}
|
||||
workingBranchName={selectedWorkspace?.branch ?? ''}
|
||||
onWorkingBranchNameChange={handleBranchNameChange}
|
||||
onActionsClick={handleActionsClick}
|
||||
onPushClick={handlePushClick}
|
||||
onOpenInEditor={handleOpenInEditor}
|
||||
onCopyPath={handleCopyPath}
|
||||
onOpenSettings={handleOpenSettings}
|
||||
onAddRepo={() => console.log('Add repo clicked')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -6,26 +6,23 @@ import {
|
||||
type LogEntry,
|
||||
} from '../VirtualizedProcessLogs';
|
||||
import { useLogStream } from '@/hooks/useLogStream';
|
||||
import { useLogsPanel } from '@/contexts/LogsPanelContext';
|
||||
|
||||
export type LogsPanelContent =
|
||||
| { type: 'process'; processId: string }
|
||||
| { type: 'tool'; toolName: string; content: string; command?: string };
|
||||
|
||||
interface LogsContentContainerProps {
|
||||
content: LogsPanelContent | null;
|
||||
className?: string;
|
||||
searchQuery?: string;
|
||||
currentMatchIndex?: number;
|
||||
onMatchIndicesChange?: (indices: number[]) => void;
|
||||
}
|
||||
|
||||
export function LogsContentContainer({
|
||||
content,
|
||||
className,
|
||||
searchQuery = '',
|
||||
currentMatchIndex = 0,
|
||||
onMatchIndicesChange,
|
||||
}: LogsContentContainerProps) {
|
||||
export function LogsContentContainer({ className }: LogsContentContainerProps) {
|
||||
const {
|
||||
logsPanelContent: content,
|
||||
logSearchQuery: searchQuery,
|
||||
logCurrentMatchIdx: currentMatchIndex,
|
||||
setLogMatchIndices: onMatchIndicesChange,
|
||||
} = useLogsPanel();
|
||||
const { t } = useTranslation('common');
|
||||
// Get logs for process content (only when type is 'process')
|
||||
const processId = content?.type === 'process' ? content.processId : '';
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
type ScreenSize,
|
||||
} from '@/hooks/usePreviewSettings';
|
||||
import { useLogStream } from '@/hooks/useLogStream';
|
||||
import { useLayoutStore } from '@/stores/useLayoutStore';
|
||||
import { useUiPreferencesStore } from '@/stores/useUiPreferencesStore';
|
||||
import { useWorkspaceContext } from '@/contexts/WorkspaceContext';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { ScriptFixerDialog } from '@/components/dialogs/scripts/ScriptFixerDialog';
|
||||
@@ -25,8 +25,10 @@ export function PreviewBrowserContainer({
|
||||
className,
|
||||
}: PreviewBrowserContainerProps) {
|
||||
const navigate = useNavigate();
|
||||
const previewRefreshKey = useLayoutStore((s) => s.previewRefreshKey);
|
||||
const triggerPreviewRefresh = useLayoutStore((s) => s.triggerPreviewRefresh);
|
||||
const previewRefreshKey = useUiPreferencesStore((s) => s.previewRefreshKey);
|
||||
const triggerPreviewRefresh = useUiPreferencesStore(
|
||||
(s) => s.triggerPreviewRefresh
|
||||
);
|
||||
const { repos, workspaceId } = useWorkspaceContext();
|
||||
|
||||
const {
|
||||
|
||||
@@ -2,7 +2,10 @@ import { useCallback, useState, useEffect } from 'react';
|
||||
import { PreviewControls } from '../views/PreviewControls';
|
||||
import { usePreviewDevServer } from '../hooks/usePreviewDevServer';
|
||||
import { useLogStream } from '@/hooks/useLogStream';
|
||||
import { useLayoutStore } from '@/stores/useLayoutStore';
|
||||
import {
|
||||
useUiPreferencesStore,
|
||||
RIGHT_MAIN_PANEL_MODES,
|
||||
} from '@/stores/useUiPreferencesStore';
|
||||
import { useWorkspaceContext } from '@/contexts/WorkspaceContext';
|
||||
|
||||
interface PreviewControlsContainerProps {
|
||||
@@ -17,7 +20,9 @@ export function PreviewControlsContainer({
|
||||
className,
|
||||
}: PreviewControlsContainerProps) {
|
||||
const { repos } = useWorkspaceContext();
|
||||
const setLogsMode = useLayoutStore((s) => s.setLogsMode);
|
||||
const setRightMainPanelMode = useUiPreferencesStore(
|
||||
(s) => s.setRightMainPanelMode
|
||||
);
|
||||
|
||||
const { isStarting, runningDevServers, devServerProcesses } =
|
||||
usePreviewDevServer(attemptId);
|
||||
@@ -42,10 +47,10 @@ export function PreviewControlsContainer({
|
||||
if (targetId && onViewProcessInPanel) {
|
||||
onViewProcessInPanel(targetId);
|
||||
} else {
|
||||
setLogsMode(true);
|
||||
setRightMainPanelMode(RIGHT_MAIN_PANEL_MODES.LOGS);
|
||||
}
|
||||
},
|
||||
[activeProcess?.id, onViewProcessInPanel, setLogsMode]
|
||||
[activeProcess?.id, onViewProcessInPanel, setRightMainPanelMode]
|
||||
);
|
||||
|
||||
const handleTabChange = useCallback((processId: string) => {
|
||||
|
||||
@@ -1,36 +1,29 @@
|
||||
import { useEffect, useMemo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useExecutionProcessesContext } from '@/contexts/ExecutionProcessesContext';
|
||||
import { useLogsPanel } from '@/contexts/LogsPanelContext';
|
||||
import { ProcessListItem } from '../primitives/ProcessListItem';
|
||||
import { CollapsibleSectionHeader } from '../primitives/CollapsibleSectionHeader';
|
||||
import { InputField } from '../primitives/InputField';
|
||||
import { CaretUpIcon, CaretDownIcon } from '@phosphor-icons/react';
|
||||
import { PERSIST_KEYS } from '@/stores/useUiPreferencesStore';
|
||||
|
||||
interface ProcessListContainerProps {
|
||||
selectedProcessId: string | null;
|
||||
onSelectProcess: (processId: string) => void;
|
||||
disableAutoSelect?: boolean;
|
||||
// Search props
|
||||
searchQuery?: string;
|
||||
onSearchQueryChange?: (query: string) => void;
|
||||
matchCount?: number;
|
||||
currentMatchIdx?: number;
|
||||
onPrevMatch?: () => void;
|
||||
onNextMatch?: () => void;
|
||||
}
|
||||
export function ProcessListContainer() {
|
||||
const {
|
||||
logsPanelContent,
|
||||
logSearchQuery: searchQuery,
|
||||
logMatchIndices,
|
||||
logCurrentMatchIdx: currentMatchIdx,
|
||||
setLogSearchQuery: onSearchQueryChange,
|
||||
handleLogPrevMatch: onPrevMatch,
|
||||
handleLogNextMatch: onNextMatch,
|
||||
viewProcessInPanel: onSelectProcess,
|
||||
} = useLogsPanel();
|
||||
|
||||
export function ProcessListContainer({
|
||||
selectedProcessId,
|
||||
onSelectProcess,
|
||||
disableAutoSelect,
|
||||
searchQuery = '',
|
||||
onSearchQueryChange,
|
||||
matchCount = 0,
|
||||
currentMatchIdx = 0,
|
||||
onPrevMatch,
|
||||
onNextMatch,
|
||||
}: ProcessListContainerProps) {
|
||||
const selectedProcessId =
|
||||
logsPanelContent?.type === 'process' ? logsPanelContent.processId : null;
|
||||
const disableAutoSelect = logsPanelContent?.type === 'tool';
|
||||
const matchCount = logMatchIndices.length;
|
||||
const { t } = useTranslation('common');
|
||||
const { executionProcessesVisible } = useExecutionProcessesContext();
|
||||
|
||||
|
||||
107
frontend/src/components/ui-new/containers/RightSidebar.tsx
Normal file
107
frontend/src/components/ui-new/containers/RightSidebar.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import { GitPanelCreateContainer } from '@/components/ui-new/containers/GitPanelCreateContainer';
|
||||
import { FileTreeContainer } from '@/components/ui-new/containers/FileTreeContainer';
|
||||
import { ProcessListContainer } from '@/components/ui-new/containers/ProcessListContainer';
|
||||
import { PreviewControlsContainer } from '@/components/ui-new/containers/PreviewControlsContainer';
|
||||
import { GitPanelContainer } from '@/components/ui-new/containers/GitPanelContainer';
|
||||
import { useChangesView } from '@/contexts/ChangesViewContext';
|
||||
import { useLogsPanel } from '@/contexts/LogsPanelContext';
|
||||
import { useWorkspaceContext } from '@/contexts/WorkspaceContext';
|
||||
import type { Workspace, RepoWithTargetBranch } from 'shared/types';
|
||||
import {
|
||||
RIGHT_MAIN_PANEL_MODES,
|
||||
type RightMainPanelMode,
|
||||
useExpandedAll,
|
||||
} from '@/stores/useUiPreferencesStore';
|
||||
|
||||
export interface RightSidebarProps {
|
||||
isCreateMode: boolean;
|
||||
rightMainPanelMode: RightMainPanelMode | null;
|
||||
selectedWorkspace: Workspace | undefined;
|
||||
repos: RepoWithTargetBranch[];
|
||||
}
|
||||
|
||||
export function RightSidebar({
|
||||
isCreateMode,
|
||||
rightMainPanelMode,
|
||||
selectedWorkspace,
|
||||
repos,
|
||||
}: RightSidebarProps) {
|
||||
const { selectFile } = useChangesView();
|
||||
const { viewProcessInPanel } = useLogsPanel();
|
||||
const { diffs } = useWorkspaceContext();
|
||||
const { setExpanded } = useExpandedAll();
|
||||
|
||||
if (isCreateMode) {
|
||||
return <GitPanelCreateContainer />;
|
||||
}
|
||||
|
||||
if (rightMainPanelMode === RIGHT_MAIN_PANEL_MODES.CHANGES) {
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex-[7] min-h-0 overflow-hidden">
|
||||
<FileTreeContainer
|
||||
key={selectedWorkspace?.id}
|
||||
workspaceId={selectedWorkspace?.id}
|
||||
diffs={diffs}
|
||||
onSelectFile={(path) => {
|
||||
selectFile(path);
|
||||
setExpanded(`diff:${path}`, true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-[3] min-h-0 overflow-hidden">
|
||||
<GitPanelContainer
|
||||
selectedWorkspace={selectedWorkspace}
|
||||
repos={repos}
|
||||
diffs={diffs}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (rightMainPanelMode === RIGHT_MAIN_PANEL_MODES.LOGS) {
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex-[7] min-h-0 overflow-hidden">
|
||||
<ProcessListContainer />
|
||||
</div>
|
||||
<div className="flex-[3] min-h-0 overflow-hidden">
|
||||
<GitPanelContainer
|
||||
selectedWorkspace={selectedWorkspace}
|
||||
repos={repos}
|
||||
diffs={diffs}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (rightMainPanelMode === RIGHT_MAIN_PANEL_MODES.PREVIEW) {
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex-[7] min-h-0 overflow-hidden">
|
||||
<PreviewControlsContainer
|
||||
attemptId={selectedWorkspace?.id}
|
||||
onViewProcessInPanel={viewProcessInPanel}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-[3] min-h-0 overflow-hidden">
|
||||
<GitPanelContainer
|
||||
selectedWorkspace={selectedWorkspace}
|
||||
repos={repos}
|
||||
diffs={diffs}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<GitPanelContainer
|
||||
selectedWorkspace={selectedWorkspace}
|
||||
repos={repos}
|
||||
diffs={diffs}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -28,6 +28,10 @@ import {
|
||||
SessionChatBox,
|
||||
type ExecutionStatus,
|
||||
} from '../primitives/SessionChatBox';
|
||||
import {
|
||||
useUiPreferencesStore,
|
||||
RIGHT_MAIN_PANEL_MODES,
|
||||
} from '@/stores/useUiPreferencesStore';
|
||||
import { Actions, type ActionDefinition } from '../actions';
|
||||
import {
|
||||
isActionVisible,
|
||||
@@ -65,8 +69,6 @@ interface SessionChatBoxContainerProps {
|
||||
linesAdded?: number;
|
||||
/** Number of lines removed */
|
||||
linesRemoved?: number;
|
||||
/** Callback to view code changes (toggle ChangesPanel) */
|
||||
onViewCode?: () => void;
|
||||
/** Available sessions for this workspace */
|
||||
sessions?: Session[];
|
||||
/** Called when a session is selected */
|
||||
@@ -87,7 +89,6 @@ export function SessionChatBoxContainer({
|
||||
filesChanged,
|
||||
linesAdded,
|
||||
linesRemoved,
|
||||
onViewCode,
|
||||
sessions = [],
|
||||
onSelectSession,
|
||||
projectId,
|
||||
@@ -101,6 +102,15 @@ export function SessionChatBoxContainer({
|
||||
|
||||
const { executeAction } = useActions();
|
||||
const actionCtx = useActionVisibilityContext();
|
||||
const { rightMainPanelMode, setRightMainPanelMode } = useUiPreferencesStore();
|
||||
|
||||
const handleViewCode = useCallback(() => {
|
||||
setRightMainPanelMode(
|
||||
rightMainPanelMode === RIGHT_MAIN_PANEL_MODES.CHANGES
|
||||
? null
|
||||
: RIGHT_MAIN_PANEL_MODES.CHANGES
|
||||
);
|
||||
}, [rightMainPanelMode, setRightMainPanelMode]);
|
||||
|
||||
// Get entries early to extract pending approval for scratch key
|
||||
const { entries } = useEntries();
|
||||
@@ -578,8 +588,8 @@ export function SessionChatBoxContainer({
|
||||
filesChanged: 0,
|
||||
linesAdded: 0,
|
||||
linesRemoved: 0,
|
||||
onViewCode: undefined,
|
||||
}}
|
||||
onViewCode={handleViewCode}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -587,6 +597,7 @@ export function SessionChatBoxContainer({
|
||||
return (
|
||||
<SessionChatBox
|
||||
status={status}
|
||||
onViewCode={handleViewCode}
|
||||
workspaceId={workspaceId}
|
||||
projectId={projectId}
|
||||
editor={{
|
||||
@@ -621,7 +632,6 @@ export function SessionChatBoxContainer({
|
||||
filesChanged,
|
||||
linesAdded,
|
||||
linesRemoved,
|
||||
onViewCode,
|
||||
hasConflicts,
|
||||
conflictedFilesCount,
|
||||
}}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,12 +3,7 @@ import type { Workspace, Session } from 'shared/types';
|
||||
import { createWorkspaceWithSession } from '@/types/attempt';
|
||||
import { WorkspacesMain } from '@/components/ui-new/views/WorkspacesMain';
|
||||
import { useTask } from '@/hooks/useTask';
|
||||
|
||||
interface DiffStats {
|
||||
filesChanged: number;
|
||||
linesAdded: number;
|
||||
linesRemoved: number;
|
||||
}
|
||||
import { useWorkspaceContext } from '@/contexts/WorkspaceContext';
|
||||
|
||||
interface WorkspacesMainContainerProps {
|
||||
selectedWorkspace: Workspace | null;
|
||||
@@ -20,10 +15,6 @@ interface WorkspacesMainContainerProps {
|
||||
isNewSessionMode?: boolean;
|
||||
/** Callback to start new session mode */
|
||||
onStartNewSession?: () => void;
|
||||
/** Callback to toggle changes panel */
|
||||
onViewCode?: () => void;
|
||||
/** Diff statistics from the workspace */
|
||||
diffStats?: DiffStats;
|
||||
}
|
||||
|
||||
export function WorkspacesMainContainer({
|
||||
@@ -34,9 +25,8 @@ export function WorkspacesMainContainer({
|
||||
isLoading,
|
||||
isNewSessionMode,
|
||||
onStartNewSession,
|
||||
onViewCode,
|
||||
diffStats,
|
||||
}: WorkspacesMainContainerProps) {
|
||||
const { diffStats } = useWorkspaceContext();
|
||||
const containerRef = useRef<HTMLElement>(null);
|
||||
|
||||
// Fetch task to get project_id for file search
|
||||
@@ -58,10 +48,13 @@ export function WorkspacesMainContainer({
|
||||
isLoading={isLoading}
|
||||
containerRef={containerRef}
|
||||
projectId={task?.project_id}
|
||||
onViewCode={onViewCode}
|
||||
isNewSessionMode={isNewSessionMode}
|
||||
onStartNewSession={onStartNewSession}
|
||||
diffStats={diffStats}
|
||||
diffStats={{
|
||||
filesChanged: diffStats.files_changed,
|
||||
linesAdded: diffStats.lines_added,
|
||||
linesRemoved: diffStats.lines_removed,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
import { useWorkspaceContext } from '@/contexts/WorkspaceContext';
|
||||
import { useActions } from '@/contexts/ActionsContext';
|
||||
import { useScratch } from '@/hooks/useScratch';
|
||||
import { ScratchType, type DraftWorkspaceData } from 'shared/types';
|
||||
import { splitMessageToTitleDescription } from '@/utils/string';
|
||||
import {
|
||||
PERSIST_KEYS,
|
||||
usePersistedExpanded,
|
||||
} from '@/stores/useUiPreferencesStore';
|
||||
import { WorkspacesSidebar } from '@/components/ui-new/views/WorkspacesSidebar';
|
||||
import { Actions } from '@/components/ui-new/actions';
|
||||
|
||||
// Fixed UUID for the universal workspace draft (same as in useCreateModeState.ts)
|
||||
const DRAFT_WORKSPACE_ID = '00000000-0000-0000-0000-000000000001';
|
||||
|
||||
export function WorkspacesSidebarContainer() {
|
||||
const {
|
||||
workspaceId: selectedWorkspaceId,
|
||||
activeWorkspaces,
|
||||
archivedWorkspaces,
|
||||
isCreateMode,
|
||||
selectWorkspace,
|
||||
navigateToCreate,
|
||||
} = useWorkspaceContext();
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [showArchive, setShowArchive] = usePersistedExpanded(
|
||||
PERSIST_KEYS.workspacesSidebarArchived,
|
||||
false
|
||||
);
|
||||
|
||||
// Read persisted draft for sidebar placeholder
|
||||
const { scratch: draftScratch } = useScratch(
|
||||
ScratchType.DRAFT_WORKSPACE,
|
||||
DRAFT_WORKSPACE_ID
|
||||
);
|
||||
|
||||
// Extract draft title from persisted scratch
|
||||
const persistedDraftTitle = useMemo(() => {
|
||||
const scratchData: DraftWorkspaceData | undefined =
|
||||
draftScratch?.payload?.type === 'DRAFT_WORKSPACE'
|
||||
? draftScratch.payload.data
|
||||
: undefined;
|
||||
|
||||
if (!scratchData?.message?.trim()) return undefined;
|
||||
const { title } = splitMessageToTitleDescription(
|
||||
scratchData.message.trim()
|
||||
);
|
||||
return title || 'New Workspace';
|
||||
}, [draftScratch]);
|
||||
|
||||
// Action handlers for sidebar workspace actions
|
||||
const { executeAction } = useActions();
|
||||
|
||||
const handleArchiveWorkspace = useCallback(
|
||||
(workspaceId: string) => {
|
||||
executeAction(Actions.ArchiveWorkspace, workspaceId);
|
||||
},
|
||||
[executeAction]
|
||||
);
|
||||
|
||||
const handlePinWorkspace = useCallback(
|
||||
(workspaceId: string) => {
|
||||
executeAction(Actions.PinWorkspace, workspaceId);
|
||||
},
|
||||
[executeAction]
|
||||
);
|
||||
|
||||
return (
|
||||
<WorkspacesSidebar
|
||||
workspaces={activeWorkspaces}
|
||||
archivedWorkspaces={archivedWorkspaces}
|
||||
selectedWorkspaceId={selectedWorkspaceId ?? null}
|
||||
onSelectWorkspace={selectWorkspace}
|
||||
searchQuery={searchQuery}
|
||||
onSearchChange={setSearchQuery}
|
||||
onAddWorkspace={navigateToCreate}
|
||||
onArchiveWorkspace={handleArchiveWorkspace}
|
||||
onPinWorkspace={handlePinWorkspace}
|
||||
isCreateMode={isCreateMode}
|
||||
draftTitle={persistedDraftTitle}
|
||||
onSelectCreate={navigateToCreate}
|
||||
showArchive={showArchive}
|
||||
onShowArchiveChange={setShowArchive}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -77,7 +77,6 @@ interface StatsProps {
|
||||
filesChanged?: number;
|
||||
linesAdded?: number;
|
||||
linesRemoved?: number;
|
||||
onViewCode?: () => void;
|
||||
hasConflicts?: boolean;
|
||||
conflictedFilesCount?: number;
|
||||
}
|
||||
@@ -135,6 +134,7 @@ interface SessionChatBoxProps {
|
||||
executor?: ExecutorProps;
|
||||
inProgressTodo?: TodoItem | null;
|
||||
localImages?: LocalImageMetadata[];
|
||||
onViewCode?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -160,6 +160,7 @@ export function SessionChatBox({
|
||||
executor,
|
||||
inProgressTodo,
|
||||
localImages,
|
||||
onViewCode,
|
||||
}: SessionChatBoxProps) {
|
||||
const { t } = useTranslation('tasks');
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -554,7 +555,7 @@ export function SessionChatBox({
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
<PrimaryButton variant="tertiary" onClick={stats?.onViewCode}>
|
||||
<PrimaryButton variant="tertiary" onClick={onViewCode}>
|
||||
<span className="text-sm space-x-half">
|
||||
<span>
|
||||
{t('diff.filesChanged', { count: filesChanged })}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import WYSIWYGEditor from '@/components/ui/wysiwyg';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useFileNavigation } from '@/contexts/FileNavigationContext';
|
||||
import { useChangesView } from '@/contexts/ChangesViewContext';
|
||||
|
||||
interface ChatMarkdownProps {
|
||||
content: string;
|
||||
@@ -15,7 +15,7 @@ export function ChatMarkdown({
|
||||
className,
|
||||
workspaceId,
|
||||
}: ChatMarkdownProps) {
|
||||
const { viewFileInChanges, findMatchingDiffPath } = useFileNavigation();
|
||||
const { viewFileInChanges, findMatchingDiffPath } = useChangesView();
|
||||
|
||||
return (
|
||||
<div className={cn('text-sm', className)} style={{ maxWidth }}>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { TerminalIcon, WrenchIcon } from '@phosphor-icons/react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { ToolStatus } from 'shared/types';
|
||||
import { ToolStatusDot } from './ToolStatusDot';
|
||||
import { useLogNavigation } from '@/contexts/LogNavigationContext';
|
||||
import { useLogsPanel } from '@/contexts/LogsPanelContext';
|
||||
|
||||
interface ChatScriptEntryProps {
|
||||
title: string;
|
||||
@@ -23,7 +23,7 @@ export function ChatScriptEntry({
|
||||
onFix,
|
||||
}: ChatScriptEntryProps) {
|
||||
const { t } = useTranslation('tasks');
|
||||
const { viewProcessInPanel } = useLogNavigation();
|
||||
const { viewProcessInPanel } = useLogsPanel();
|
||||
const isRunning = status.status === 'created';
|
||||
const isSuccess = status.status === 'success';
|
||||
const isFailed = status.status === 'failed';
|
||||
|
||||
@@ -23,7 +23,6 @@ interface WorkspacesMainProps {
|
||||
isLoading: boolean;
|
||||
containerRef: RefObject<HTMLElement | null>;
|
||||
projectId?: string;
|
||||
onViewCode?: () => void;
|
||||
/** Whether user is creating a new session */
|
||||
isNewSessionMode?: boolean;
|
||||
/** Callback to start new session mode */
|
||||
@@ -39,7 +38,6 @@ export function WorkspacesMain({
|
||||
isLoading,
|
||||
containerRef,
|
||||
projectId,
|
||||
onViewCode,
|
||||
isNewSessionMode,
|
||||
onStartNewSession,
|
||||
diffStats,
|
||||
@@ -91,7 +89,6 @@ export function WorkspacesMain({
|
||||
filesChanged={diffStats?.filesChanged}
|
||||
linesAdded={diffStats?.linesAdded}
|
||||
linesRemoved={diffStats?.linesRemoved}
|
||||
onViewCode={onViewCode}
|
||||
projectId={projectId}
|
||||
isNewSessionMode={isNewSessionMode}
|
||||
onStartNewSession={onStartNewSession}
|
||||
|
||||
@@ -56,8 +56,6 @@ export function WorkspacesSidebar({
|
||||
.filter((workspace) => workspace.name.toLowerCase().includes(searchLower))
|
||||
.slice(0, isSearching ? undefined : DISPLAY_LIMIT);
|
||||
|
||||
const hasArchivedWorkspaces = archivedWorkspaces.length > 0;
|
||||
|
||||
return (
|
||||
<div className="w-full h-full bg-secondary flex flex-col">
|
||||
{/* Header + Search */}
|
||||
@@ -156,7 +154,6 @@ export function WorkspacesSidebar({
|
||||
</div>
|
||||
|
||||
{/* Fixed footer toggle - only show if there are archived workspaces */}
|
||||
{hasArchivedWorkspaces && (
|
||||
<div className="border-t border-primary p-base">
|
||||
<button
|
||||
onClick={() => onShowArchiveChange?.(!showArchive)}
|
||||
@@ -178,7 +175,6 @@ export function WorkspacesSidebar({
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
110
frontend/src/contexts/ChangesViewContext.tsx
Normal file
110
frontend/src/contexts/ChangesViewContext.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useState,
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import {
|
||||
useUiPreferencesStore,
|
||||
RIGHT_MAIN_PANEL_MODES,
|
||||
} from '@/stores/useUiPreferencesStore';
|
||||
import { useWorkspaceContext } from '@/contexts/WorkspaceContext';
|
||||
|
||||
interface ChangesViewContextValue {
|
||||
/** File path selected by user (triggers scroll-to in ChangesPanelContainer) */
|
||||
selectedFilePath: string | null;
|
||||
/** File currently in view from scrolling (for FileTree highlighting) */
|
||||
fileInView: string | null;
|
||||
/** Select a file and update fileInView */
|
||||
selectFile: (path: string) => void;
|
||||
/** Update the file currently in view (from scroll observer) */
|
||||
setFileInView: (path: string | null) => void;
|
||||
/** Navigate to changes mode and scroll to a specific file */
|
||||
viewFileInChanges: (filePath: string) => void;
|
||||
/** Set of file paths currently in the diffs (for checking if inline code should be clickable) */
|
||||
diffPaths: Set<string>;
|
||||
/** Find a diff path matching the given text (supports partial/right-hand match) */
|
||||
findMatchingDiffPath: (text: string) => string | null;
|
||||
}
|
||||
|
||||
const EMPTY_SET = new Set<string>();
|
||||
|
||||
const defaultValue: ChangesViewContextValue = {
|
||||
selectedFilePath: null,
|
||||
fileInView: null,
|
||||
selectFile: () => {},
|
||||
setFileInView: () => {},
|
||||
viewFileInChanges: () => {},
|
||||
diffPaths: EMPTY_SET,
|
||||
findMatchingDiffPath: () => null,
|
||||
};
|
||||
|
||||
const ChangesViewContext = createContext<ChangesViewContextValue>(defaultValue);
|
||||
|
||||
interface ChangesViewProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function ChangesViewProvider({ children }: ChangesViewProviderProps) {
|
||||
const { diffPaths } = useWorkspaceContext();
|
||||
const [selectedFilePath, setSelectedFilePath] = useState<string | null>(null);
|
||||
const [fileInView, setFileInView] = useState<string | null>(null);
|
||||
const { setRightMainPanelMode } = useUiPreferencesStore();
|
||||
|
||||
const selectFile = useCallback((path: string) => {
|
||||
setSelectedFilePath(path);
|
||||
setFileInView(path);
|
||||
}, []);
|
||||
|
||||
const viewFileInChanges = useCallback(
|
||||
(filePath: string) => {
|
||||
setRightMainPanelMode(RIGHT_MAIN_PANEL_MODES.CHANGES);
|
||||
setSelectedFilePath(filePath);
|
||||
},
|
||||
[setRightMainPanelMode]
|
||||
);
|
||||
|
||||
const findMatchingDiffPath = useCallback(
|
||||
(text: string): string | null => {
|
||||
if (diffPaths.has(text)) return text;
|
||||
for (const fullPath of diffPaths) {
|
||||
if (fullPath.endsWith('/' + text)) {
|
||||
return fullPath;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
[diffPaths]
|
||||
);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
selectedFilePath,
|
||||
fileInView,
|
||||
selectFile,
|
||||
setFileInView,
|
||||
viewFileInChanges,
|
||||
diffPaths,
|
||||
findMatchingDiffPath,
|
||||
}),
|
||||
[
|
||||
selectedFilePath,
|
||||
fileInView,
|
||||
selectFile,
|
||||
viewFileInChanges,
|
||||
diffPaths,
|
||||
findMatchingDiffPath,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<ChangesViewContext.Provider value={value}>
|
||||
{children}
|
||||
</ChangesViewContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useChangesView(): ChangesViewContextValue {
|
||||
return useContext(ChangesViewContext);
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
import { createContext, useContext, useMemo, type ReactNode } from 'react';
|
||||
import type {
|
||||
Repo,
|
||||
ExecutorProfileId,
|
||||
RepoWithTargetBranch,
|
||||
} from 'shared/types';
|
||||
import type { Repo, ExecutorProfileId } from 'shared/types';
|
||||
import { useCreateModeState } from '@/hooks/useCreateModeState';
|
||||
import { useWorkspaces } from '@/components/ui-new/hooks/useWorkspaces';
|
||||
import { useTask } from '@/hooks/useTask';
|
||||
import { useAttemptRepo } from '@/hooks/useAttemptRepo';
|
||||
|
||||
interface CreateModeContextValue {
|
||||
selectedProjectId: string | null;
|
||||
@@ -28,16 +27,28 @@ const CreateModeContext = createContext<CreateModeContextValue | null>(null);
|
||||
|
||||
interface CreateModeProviderProps {
|
||||
children: ReactNode;
|
||||
initialProjectId?: string;
|
||||
initialRepos?: RepoWithTargetBranch[];
|
||||
}
|
||||
|
||||
export function CreateModeProvider({
|
||||
children,
|
||||
initialProjectId,
|
||||
initialRepos,
|
||||
}: CreateModeProviderProps) {
|
||||
const state = useCreateModeState({ initialProjectId, initialRepos });
|
||||
export function CreateModeProvider({ children }: CreateModeProviderProps) {
|
||||
// Fetch most recent workspace to use as initial values
|
||||
const { workspaces: activeWorkspaces, archivedWorkspaces } = useWorkspaces();
|
||||
const mostRecentWorkspace = activeWorkspaces[0] ?? archivedWorkspaces[0];
|
||||
|
||||
const { data: lastWorkspaceTask } = useTask(mostRecentWorkspace?.taskId, {
|
||||
enabled: !!mostRecentWorkspace?.taskId,
|
||||
});
|
||||
|
||||
const { repos: lastWorkspaceRepos } = useAttemptRepo(
|
||||
mostRecentWorkspace?.id,
|
||||
{
|
||||
enabled: !!mostRecentWorkspace?.id,
|
||||
}
|
||||
);
|
||||
|
||||
const state = useCreateModeState({
|
||||
initialProjectId: lastWorkspaceTask?.project_id,
|
||||
initialRepos: lastWorkspaceRepos,
|
||||
});
|
||||
|
||||
const value = useMemo<CreateModeContextValue>(
|
||||
() => ({
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import React, { createContext, useContext, useMemo, useCallback } from 'react';
|
||||
|
||||
interface FileNavigationContextValue {
|
||||
/** Navigate to changes panel and scroll to the specified file */
|
||||
viewFileInChanges: (path: string) => void;
|
||||
/** Set of file paths currently in the diffs (for checking if inline code should be clickable) */
|
||||
diffPaths: Set<string>;
|
||||
/** Find a diff path matching the given text (supports partial/right-hand match) */
|
||||
findMatchingDiffPath: (text: string) => string | null;
|
||||
}
|
||||
|
||||
const EMPTY_SET = new Set<string>();
|
||||
|
||||
const defaultValue: FileNavigationContextValue = {
|
||||
viewFileInChanges: () => {},
|
||||
diffPaths: EMPTY_SET,
|
||||
findMatchingDiffPath: () => null,
|
||||
};
|
||||
|
||||
const FileNavigationContext =
|
||||
createContext<FileNavigationContextValue>(defaultValue);
|
||||
|
||||
interface FileNavigationProviderProps {
|
||||
children: React.ReactNode;
|
||||
viewFileInChanges: (path: string) => void;
|
||||
diffPaths: Set<string>;
|
||||
}
|
||||
|
||||
export function FileNavigationProvider({
|
||||
children,
|
||||
viewFileInChanges,
|
||||
diffPaths,
|
||||
}: FileNavigationProviderProps) {
|
||||
// Find a diff path that matches the given text (exact or right-hand match)
|
||||
const findMatchingDiffPath = useCallback(
|
||||
(text: string): string | null => {
|
||||
// Exact match first
|
||||
if (diffPaths.has(text)) return text;
|
||||
|
||||
// Right-hand match: check if any diff path ends with the text
|
||||
// Must match at a path boundary (after / or at start)
|
||||
for (const fullPath of diffPaths) {
|
||||
if (fullPath.endsWith('/' + text)) {
|
||||
return fullPath;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
[diffPaths]
|
||||
);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({ viewFileInChanges, diffPaths, findMatchingDiffPath }),
|
||||
[viewFileInChanges, diffPaths, findMatchingDiffPath]
|
||||
);
|
||||
|
||||
return (
|
||||
<FileNavigationContext.Provider value={value}>
|
||||
{children}
|
||||
</FileNavigationContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useFileNavigation(): FileNavigationContextValue {
|
||||
return useContext(FileNavigationContext);
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import React, { createContext, useContext, useMemo } from 'react';
|
||||
|
||||
interface LogNavigationContextValue {
|
||||
/** Navigate to logs panel and select the specified process */
|
||||
viewProcessInPanel: (processId: string) => void;
|
||||
/** Navigate to logs panel and display static tool content */
|
||||
viewToolContentInPanel: (
|
||||
toolName: string,
|
||||
content: string,
|
||||
command?: string
|
||||
) => void;
|
||||
}
|
||||
|
||||
const defaultValue: LogNavigationContextValue = {
|
||||
viewProcessInPanel: () => {},
|
||||
viewToolContentInPanel: () => {},
|
||||
};
|
||||
|
||||
const LogNavigationContext =
|
||||
createContext<LogNavigationContextValue>(defaultValue);
|
||||
|
||||
interface LogNavigationProviderProps {
|
||||
children: React.ReactNode;
|
||||
viewProcessInPanel: (processId: string) => void;
|
||||
viewToolContentInPanel: (
|
||||
toolName: string,
|
||||
content: string,
|
||||
command?: string
|
||||
) => void;
|
||||
}
|
||||
|
||||
export function LogNavigationProvider({
|
||||
children,
|
||||
viewProcessInPanel,
|
||||
viewToolContentInPanel,
|
||||
}: LogNavigationProviderProps) {
|
||||
const value = useMemo(
|
||||
() => ({ viewProcessInPanel, viewToolContentInPanel }),
|
||||
[viewProcessInPanel, viewToolContentInPanel]
|
||||
);
|
||||
|
||||
return (
|
||||
<LogNavigationContext.Provider value={value}>
|
||||
{children}
|
||||
</LogNavigationContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useLogNavigation(): LogNavigationContextValue {
|
||||
return useContext(LogNavigationContext);
|
||||
}
|
||||
145
frontend/src/contexts/LogsPanelContext.tsx
Normal file
145
frontend/src/contexts/LogsPanelContext.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useState,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
import type { LogsPanelContent } from '@/components/ui-new/containers/LogsContentContainer';
|
||||
import {
|
||||
useUiPreferencesStore,
|
||||
RIGHT_MAIN_PANEL_MODES,
|
||||
} from '@/stores/useUiPreferencesStore';
|
||||
|
||||
interface LogsPanelContextValue {
|
||||
logsPanelContent: LogsPanelContent | null;
|
||||
logSearchQuery: string;
|
||||
logMatchIndices: number[];
|
||||
logCurrentMatchIdx: number;
|
||||
setLogSearchQuery: (query: string) => void;
|
||||
setLogMatchIndices: (indices: number[]) => void;
|
||||
handleLogPrevMatch: () => void;
|
||||
handleLogNextMatch: () => void;
|
||||
viewProcessInPanel: (processId: string) => void;
|
||||
viewToolContentInPanel: (
|
||||
toolName: string,
|
||||
content: string,
|
||||
command?: string
|
||||
) => void;
|
||||
}
|
||||
|
||||
const defaultValue: LogsPanelContextValue = {
|
||||
logsPanelContent: null,
|
||||
logSearchQuery: '',
|
||||
logMatchIndices: [],
|
||||
logCurrentMatchIdx: 0,
|
||||
setLogSearchQuery: () => {},
|
||||
setLogMatchIndices: () => {},
|
||||
handleLogPrevMatch: () => {},
|
||||
handleLogNextMatch: () => {},
|
||||
viewProcessInPanel: () => {},
|
||||
viewToolContentInPanel: () => {},
|
||||
};
|
||||
|
||||
const LogsPanelContext = createContext<LogsPanelContextValue>(defaultValue);
|
||||
|
||||
interface LogsPanelProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function LogsPanelProvider({ children }: LogsPanelProviderProps) {
|
||||
const { rightMainPanelMode, setRightMainPanelMode } = useUiPreferencesStore();
|
||||
|
||||
const [logsPanelContent, setLogsPanelContent] =
|
||||
useState<LogsPanelContent | null>(null);
|
||||
const [logSearchQuery, setLogSearchQuery] = useState('');
|
||||
const [logMatchIndices, setLogMatchIndices] = useState<number[]>([]);
|
||||
const [logCurrentMatchIdx, setLogCurrentMatchIdx] = useState(0);
|
||||
|
||||
const logContentId =
|
||||
logsPanelContent?.type === 'process'
|
||||
? logsPanelContent.processId
|
||||
: logsPanelContent?.type === 'tool'
|
||||
? logsPanelContent.toolName
|
||||
: null;
|
||||
|
||||
useEffect(() => {
|
||||
setLogSearchQuery('');
|
||||
setLogCurrentMatchIdx(0);
|
||||
}, [logContentId]);
|
||||
|
||||
useEffect(() => {
|
||||
setLogCurrentMatchIdx(0);
|
||||
}, [logSearchQuery]);
|
||||
|
||||
const handleLogPrevMatch = useCallback(() => {
|
||||
if (logMatchIndices.length === 0) return;
|
||||
setLogCurrentMatchIdx((prev) =>
|
||||
prev > 0 ? prev - 1 : logMatchIndices.length - 1
|
||||
);
|
||||
}, [logMatchIndices.length]);
|
||||
|
||||
const handleLogNextMatch = useCallback(() => {
|
||||
if (logMatchIndices.length === 0) return;
|
||||
setLogCurrentMatchIdx((prev) =>
|
||||
prev < logMatchIndices.length - 1 ? prev + 1 : 0
|
||||
);
|
||||
}, [logMatchIndices.length]);
|
||||
|
||||
const viewProcessInPanel = useCallback(
|
||||
(processId: string) => {
|
||||
if (rightMainPanelMode !== RIGHT_MAIN_PANEL_MODES.LOGS) {
|
||||
setRightMainPanelMode(RIGHT_MAIN_PANEL_MODES.LOGS);
|
||||
}
|
||||
setLogsPanelContent({ type: 'process', processId });
|
||||
},
|
||||
[rightMainPanelMode, setRightMainPanelMode]
|
||||
);
|
||||
|
||||
const viewToolContentInPanel = useCallback(
|
||||
(toolName: string, content: string, command?: string) => {
|
||||
if (rightMainPanelMode !== RIGHT_MAIN_PANEL_MODES.LOGS) {
|
||||
setRightMainPanelMode(RIGHT_MAIN_PANEL_MODES.LOGS);
|
||||
}
|
||||
setLogsPanelContent({ type: 'tool', toolName, content, command });
|
||||
},
|
||||
[rightMainPanelMode, setRightMainPanelMode]
|
||||
);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
logsPanelContent,
|
||||
logSearchQuery,
|
||||
logMatchIndices,
|
||||
logCurrentMatchIdx,
|
||||
setLogSearchQuery,
|
||||
setLogMatchIndices,
|
||||
handleLogPrevMatch,
|
||||
handleLogNextMatch,
|
||||
viewProcessInPanel,
|
||||
viewToolContentInPanel,
|
||||
}),
|
||||
[
|
||||
logsPanelContent,
|
||||
logSearchQuery,
|
||||
logMatchIndices,
|
||||
logCurrentMatchIdx,
|
||||
handleLogPrevMatch,
|
||||
handleLogNextMatch,
|
||||
viewProcessInPanel,
|
||||
viewToolContentInPanel,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<LogsPanelContext.Provider value={value}>
|
||||
{children}
|
||||
</LogsPanelContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useLogsPanel(): LogsPanelContextValue {
|
||||
return useContext(LogsPanelContext);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
ReactNode,
|
||||
useMemo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
} from 'react';
|
||||
import { useParams, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
@@ -19,12 +20,17 @@ import {
|
||||
useGitHubComments,
|
||||
type NormalizedGitHubComment,
|
||||
} from '@/hooks/useGitHubComments';
|
||||
import { useDiffStream } from '@/hooks/useDiffStream';
|
||||
import { attemptsApi } from '@/lib/api';
|
||||
import { useUiPreferencesStore } from '@/stores/useUiPreferencesStore';
|
||||
import { useDiffViewStore } from '@/stores/useDiffViewStore';
|
||||
import type {
|
||||
Workspace as ApiWorkspace,
|
||||
Session,
|
||||
RepoWithTargetBranch,
|
||||
UnifiedPrComment,
|
||||
Diff,
|
||||
DiffStats,
|
||||
} from 'shared/types';
|
||||
|
||||
export type { NormalizedGitHubComment } from '@/hooks/useGitHubComments';
|
||||
@@ -62,6 +68,12 @@ interface WorkspaceContextValue {
|
||||
setShowGitHubComments: (show: boolean) => void;
|
||||
getGitHubCommentsForFile: (filePath: string) => NormalizedGitHubComment[];
|
||||
getGitHubCommentCountForFile: (filePath: string) => number;
|
||||
/** Diffs for the current workspace */
|
||||
diffs: Diff[];
|
||||
/** Set of file paths in the diffs */
|
||||
diffPaths: Set<string>;
|
||||
/** Aggregate diff statistics */
|
||||
diffStats: DiffStats;
|
||||
}
|
||||
|
||||
const WorkspaceContext = createContext<WorkspaceContextValue | null>(null);
|
||||
@@ -79,6 +91,13 @@ export function WorkspaceProvider({ children }: WorkspaceProviderProps) {
|
||||
// Derive isCreateMode from URL path instead of prop to allow provider to persist across route changes
|
||||
const isCreateMode = location.pathname === '/workspaces/create';
|
||||
|
||||
// Reset UI state when entering create mode
|
||||
useEffect(() => {
|
||||
if (isCreateMode) {
|
||||
useUiPreferencesStore.getState().resetForCreateMode();
|
||||
}
|
||||
}, [isCreateMode]);
|
||||
|
||||
// Fetch workspaces for sidebar display
|
||||
const {
|
||||
workspaces: activeWorkspaces,
|
||||
@@ -127,6 +146,30 @@ export function WorkspaceProvider({ children }: WorkspaceProviderProps) {
|
||||
enabled: !isCreateMode,
|
||||
});
|
||||
|
||||
// Stream diffs for the current workspace
|
||||
const { diffs } = useDiffStream(workspaceId ?? null, !isCreateMode);
|
||||
|
||||
const diffPaths = useMemo(
|
||||
() =>
|
||||
new Set(diffs.map((d) => d.newPath || d.oldPath || '').filter(Boolean)),
|
||||
[diffs]
|
||||
);
|
||||
|
||||
// Sync diffPaths to store for expand/collapse all functionality
|
||||
useEffect(() => {
|
||||
useDiffViewStore.getState().setDiffPaths(Array.from(diffPaths));
|
||||
return () => useDiffViewStore.getState().setDiffPaths([]);
|
||||
}, [diffPaths]);
|
||||
|
||||
const diffStats: DiffStats = useMemo(
|
||||
() => ({
|
||||
files_changed: diffs.length,
|
||||
lines_added: diffs.reduce((sum, d) => sum + (d.additions ?? 0), 0),
|
||||
lines_removed: diffs.reduce((sum, d) => sum + (d.deletions ?? 0), 0),
|
||||
}),
|
||||
[diffs]
|
||||
);
|
||||
|
||||
const isLoading = isLoadingList || isLoadingWorkspace;
|
||||
|
||||
const selectWorkspace = useCallback(
|
||||
@@ -180,6 +223,9 @@ export function WorkspaceProvider({ children }: WorkspaceProviderProps) {
|
||||
setShowGitHubComments,
|
||||
getGitHubCommentsForFile,
|
||||
getGitHubCommentCountForFile,
|
||||
diffs,
|
||||
diffPaths,
|
||||
diffStats,
|
||||
}),
|
||||
[
|
||||
workspaceId,
|
||||
@@ -206,6 +252,9 @@ export function WorkspaceProvider({ children }: WorkspaceProviderProps) {
|
||||
setShowGitHubComments,
|
||||
getGitHubCommentsForFile,
|
||||
getGitHubCommentCountForFile,
|
||||
diffs,
|
||||
diffPaths,
|
||||
diffStats,
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
type LayoutState = {
|
||||
// Panel visibility
|
||||
isSidebarVisible: boolean;
|
||||
isMainPanelVisible: boolean;
|
||||
isGitPanelVisible: boolean;
|
||||
isChangesMode: boolean;
|
||||
isLogsMode: boolean;
|
||||
isPreviewMode: boolean;
|
||||
|
||||
// Preview refresh coordination
|
||||
previewRefreshKey: number;
|
||||
|
||||
// Toggle functions
|
||||
toggleSidebar: () => void;
|
||||
toggleMainPanel: () => void;
|
||||
toggleGitPanel: () => void;
|
||||
toggleChangesMode: () => void;
|
||||
toggleLogsMode: () => void;
|
||||
togglePreviewMode: () => void;
|
||||
|
||||
// Setters for direct state updates
|
||||
setChangesMode: (value: boolean) => void;
|
||||
setLogsMode: (value: boolean) => void;
|
||||
setPreviewMode: (value: boolean) => void;
|
||||
setSidebarVisible: (value: boolean) => void;
|
||||
setMainPanelVisible: (value: boolean) => void;
|
||||
|
||||
// Preview actions
|
||||
triggerPreviewRefresh: () => void;
|
||||
|
||||
// Reset for create mode
|
||||
resetForCreateMode: () => void;
|
||||
};
|
||||
|
||||
// Check if screen is wide enough to keep sidebar visible
|
||||
const isWideScreen = () => window.innerWidth > 2048;
|
||||
|
||||
export const useLayoutStore = create<LayoutState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
isSidebarVisible: true,
|
||||
isMainPanelVisible: true,
|
||||
isGitPanelVisible: true,
|
||||
isChangesMode: false,
|
||||
isLogsMode: false,
|
||||
isPreviewMode: false,
|
||||
previewRefreshKey: 0,
|
||||
|
||||
toggleSidebar: () =>
|
||||
set((s) => ({ isSidebarVisible: !s.isSidebarVisible })),
|
||||
|
||||
toggleMainPanel: () => {
|
||||
const { isMainPanelVisible, isChangesMode } = get();
|
||||
// At least one of Main or Changes must be visible
|
||||
if (isMainPanelVisible && !isChangesMode) return;
|
||||
set({ isMainPanelVisible: !isMainPanelVisible });
|
||||
},
|
||||
|
||||
toggleGitPanel: () =>
|
||||
set((s) => ({ isGitPanelVisible: !s.isGitPanelVisible })),
|
||||
|
||||
toggleChangesMode: () => {
|
||||
const { isChangesMode } = get();
|
||||
const newChangesMode = !isChangesMode;
|
||||
|
||||
if (newChangesMode) {
|
||||
// Changes, logs, and preview are mutually exclusive
|
||||
// Auto-hide sidebar when entering changes mode (unless screen is wide enough)
|
||||
set({
|
||||
isChangesMode: true,
|
||||
isLogsMode: false,
|
||||
isPreviewMode: false,
|
||||
isSidebarVisible: isWideScreen() ? get().isSidebarVisible : false,
|
||||
});
|
||||
} else {
|
||||
// Auto-show sidebar when exiting changes mode
|
||||
set({
|
||||
isChangesMode: false,
|
||||
isSidebarVisible: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
toggleLogsMode: () => {
|
||||
const { isLogsMode } = get();
|
||||
const newLogsMode = !isLogsMode;
|
||||
|
||||
if (newLogsMode) {
|
||||
// Logs, changes, and preview are mutually exclusive
|
||||
// Auto-hide sidebar when entering logs mode (unless screen is wide enough)
|
||||
set({
|
||||
isLogsMode: true,
|
||||
isChangesMode: false,
|
||||
isPreviewMode: false,
|
||||
isSidebarVisible: isWideScreen() ? get().isSidebarVisible : false,
|
||||
});
|
||||
} else {
|
||||
// Auto-show sidebar when exiting logs mode
|
||||
set({
|
||||
isLogsMode: false,
|
||||
isSidebarVisible: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
togglePreviewMode: () => {
|
||||
const { isPreviewMode } = get();
|
||||
const newPreviewMode = !isPreviewMode;
|
||||
|
||||
if (newPreviewMode) {
|
||||
// Preview, changes, and logs are mutually exclusive
|
||||
// Auto-hide sidebar when entering preview mode (unless screen is wide enough)
|
||||
set({
|
||||
isPreviewMode: true,
|
||||
isChangesMode: false,
|
||||
isLogsMode: false,
|
||||
isSidebarVisible: isWideScreen() ? get().isSidebarVisible : false,
|
||||
});
|
||||
} else {
|
||||
// Auto-show sidebar when exiting preview mode
|
||||
set({
|
||||
isPreviewMode: false,
|
||||
isSidebarVisible: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
setChangesMode: (value) => {
|
||||
if (value) {
|
||||
set({
|
||||
isChangesMode: true,
|
||||
isLogsMode: false,
|
||||
isPreviewMode: false,
|
||||
isSidebarVisible: isWideScreen() ? get().isSidebarVisible : false,
|
||||
});
|
||||
} else {
|
||||
set({ isChangesMode: false });
|
||||
}
|
||||
},
|
||||
|
||||
setLogsMode: (value) => {
|
||||
if (value) {
|
||||
set({
|
||||
isLogsMode: true,
|
||||
isChangesMode: false,
|
||||
isPreviewMode: false,
|
||||
isSidebarVisible: isWideScreen() ? get().isSidebarVisible : false,
|
||||
});
|
||||
} else {
|
||||
set({ isLogsMode: false });
|
||||
}
|
||||
},
|
||||
|
||||
setPreviewMode: (value) => {
|
||||
if (value) {
|
||||
set({
|
||||
isPreviewMode: true,
|
||||
isChangesMode: false,
|
||||
isLogsMode: false,
|
||||
isSidebarVisible: isWideScreen() ? get().isSidebarVisible : false,
|
||||
});
|
||||
} else {
|
||||
set({ isPreviewMode: false });
|
||||
}
|
||||
},
|
||||
|
||||
setSidebarVisible: (value) => set({ isSidebarVisible: value }),
|
||||
|
||||
setMainPanelVisible: (value) => set({ isMainPanelVisible: value }),
|
||||
|
||||
triggerPreviewRefresh: () =>
|
||||
set((s) => ({ previewRefreshKey: s.previewRefreshKey + 1 })),
|
||||
|
||||
resetForCreateMode: () =>
|
||||
set({
|
||||
isChangesMode: false,
|
||||
isLogsMode: false,
|
||||
isPreviewMode: false,
|
||||
}),
|
||||
}),
|
||||
{
|
||||
name: 'layout-preferences',
|
||||
// Only persist panel visibility preferences, not mode states
|
||||
partialize: (state) => ({
|
||||
isSidebarVisible: state.isSidebarVisible,
|
||||
isMainPanelVisible: state.isMainPanelVisible,
|
||||
isGitPanelVisible: state.isGitPanelVisible,
|
||||
}),
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Convenience hooks for individual state values
|
||||
export const useIsSidebarVisible = () =>
|
||||
useLayoutStore((s) => s.isSidebarVisible);
|
||||
export const useIsMainPanelVisible = () =>
|
||||
useLayoutStore((s) => s.isMainPanelVisible);
|
||||
export const useIsGitPanelVisible = () =>
|
||||
useLayoutStore((s) => s.isGitPanelVisible);
|
||||
export const useIsChangesMode = () => useLayoutStore((s) => s.isChangesMode);
|
||||
export const useIsLogsMode = () => useLayoutStore((s) => s.isLogsMode);
|
||||
export const useIsPreviewMode = () => useLayoutStore((s) => s.isPreviewMode);
|
||||
|
||||
// Derived selector: true when right main panel content is visible (Changes/Logs/Preview)
|
||||
export const useIsRightMainPanelVisible = () =>
|
||||
useLayoutStore((s) => s.isChangesMode || s.isLogsMode || s.isPreviewMode);
|
||||
@@ -3,6 +3,15 @@ import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import type { RepoAction } from '@/components/ui-new/primitives/RepoCard';
|
||||
|
||||
export const RIGHT_MAIN_PANEL_MODES = {
|
||||
CHANGES: 'changes',
|
||||
LOGS: 'logs',
|
||||
PREVIEW: 'preview',
|
||||
} as const;
|
||||
|
||||
export type RightMainPanelMode =
|
||||
(typeof RIGHT_MAIN_PANEL_MODES)[keyof typeof RIGHT_MAIN_PANEL_MODES];
|
||||
|
||||
export type ContextBarPosition =
|
||||
| 'top-left'
|
||||
| 'top-right'
|
||||
@@ -14,11 +23,9 @@ export type ContextBarPosition =
|
||||
// Centralized persist keys for type safety
|
||||
export const PERSIST_KEYS = {
|
||||
// Sidebar sections
|
||||
workspacesSidebarActive: 'workspaces-sidebar-active',
|
||||
workspacesSidebarArchived: 'workspaces-sidebar-archived',
|
||||
// Git panel sections
|
||||
gitAdvancedSettings: 'git-advanced-settings',
|
||||
gitPanelCreateAddRepo: 'git-panel-create-add-repo',
|
||||
gitPanelRepositories: 'git-panel-repositories',
|
||||
gitPanelProject: 'git-panel-project',
|
||||
gitPanelAddRepositories: 'git-panel-add-repositories',
|
||||
@@ -28,8 +35,6 @@ export const PERSIST_KEYS = {
|
||||
changesSection: 'changes-section',
|
||||
// Preview panel sections
|
||||
devServerSection: 'dev-server-section',
|
||||
// Context bar
|
||||
contextBarPosition: 'context-bar-position',
|
||||
// GitHub comments toggle
|
||||
showGitHubComments: 'show-github-comments',
|
||||
// Panel sizes
|
||||
@@ -38,11 +43,12 @@ export const PERSIST_KEYS = {
|
||||
repoCard: (repoId: string) => `repo-card-${repoId}` as const,
|
||||
} as const;
|
||||
|
||||
// Check if screen is wide enough to keep sidebar visible
|
||||
const isWideScreen = () => window.innerWidth > 2048;
|
||||
|
||||
export type PersistKey =
|
||||
| typeof PERSIST_KEYS.workspacesSidebarActive
|
||||
| typeof PERSIST_KEYS.workspacesSidebarArchived
|
||||
| typeof PERSIST_KEYS.gitAdvancedSettings
|
||||
| typeof PERSIST_KEYS.gitPanelCreateAddRepo
|
||||
| typeof PERSIST_KEYS.gitPanelRepositories
|
||||
| typeof PERSIST_KEYS.gitPanelProject
|
||||
| typeof PERSIST_KEYS.gitPanelAddRepositories
|
||||
@@ -63,11 +69,21 @@ export type PersistKey =
|
||||
| `entry:${string}`;
|
||||
|
||||
type State = {
|
||||
// UI preferences
|
||||
repoActions: Record<string, RepoAction>;
|
||||
expanded: Record<string, boolean>;
|
||||
contextBarPosition: ContextBarPosition;
|
||||
paneSizes: Record<string, number | string>;
|
||||
collapsedPaths: Record<string, string[]>;
|
||||
|
||||
// Layout state
|
||||
isLeftSidebarVisible: boolean;
|
||||
isLeftMainPanelVisible: boolean;
|
||||
isRightSidebarVisible: boolean;
|
||||
rightMainPanelMode: RightMainPanelMode | null;
|
||||
previewRefreshKey: number;
|
||||
|
||||
// UI preferences actions
|
||||
setRepoAction: (repoId: string, action: RepoAction) => void;
|
||||
setExpanded: (key: string, value: boolean) => void;
|
||||
toggleExpanded: (key: string, defaultValue?: boolean) => void;
|
||||
@@ -75,16 +91,37 @@ type State = {
|
||||
setContextBarPosition: (position: ContextBarPosition) => void;
|
||||
setPaneSize: (key: string, size: number | string) => void;
|
||||
setCollapsedPaths: (key: string, paths: string[]) => void;
|
||||
|
||||
// Layout actions
|
||||
toggleLeftSidebar: () => void;
|
||||
toggleLeftMainPanel: () => void;
|
||||
toggleRightSidebar: () => void;
|
||||
toggleRightMainPanelMode: (mode: RightMainPanelMode) => void;
|
||||
setRightMainPanelMode: (mode: RightMainPanelMode | null) => void;
|
||||
setLeftSidebarVisible: (value: boolean) => void;
|
||||
setLeftMainPanelVisible: (value: boolean) => void;
|
||||
triggerPreviewRefresh: () => void;
|
||||
resetForCreateMode: () => void;
|
||||
};
|
||||
|
||||
export const useUiPreferencesStore = create<State>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
(set, get) => ({
|
||||
// UI preferences state
|
||||
repoActions: {},
|
||||
expanded: {},
|
||||
contextBarPosition: 'middle-right',
|
||||
paneSizes: {},
|
||||
collapsedPaths: {},
|
||||
|
||||
// Layout state
|
||||
isLeftSidebarVisible: true,
|
||||
isLeftMainPanelVisible: true,
|
||||
isRightSidebarVisible: true,
|
||||
rightMainPanelMode: null,
|
||||
previewRefreshKey: 0,
|
||||
|
||||
// UI preferences actions
|
||||
setRepoAction: (repoId, action) =>
|
||||
set((s) => ({ repoActions: { ...s.repoActions, [repoId]: action } })),
|
||||
setExpanded: (key, value) =>
|
||||
@@ -109,8 +146,80 @@ export const useUiPreferencesStore = create<State>()(
|
||||
set((s) => ({ paneSizes: { ...s.paneSizes, [key]: size } })),
|
||||
setCollapsedPaths: (key, paths) =>
|
||||
set((s) => ({ collapsedPaths: { ...s.collapsedPaths, [key]: paths } })),
|
||||
|
||||
// Layout actions
|
||||
toggleLeftSidebar: () =>
|
||||
set((s) => ({ isLeftSidebarVisible: !s.isLeftSidebarVisible })),
|
||||
|
||||
toggleLeftMainPanel: () => {
|
||||
const { isLeftMainPanelVisible, rightMainPanelMode } = get();
|
||||
if (isLeftMainPanelVisible && rightMainPanelMode === null) return;
|
||||
set({ isLeftMainPanelVisible: !isLeftMainPanelVisible });
|
||||
},
|
||||
|
||||
toggleRightSidebar: () =>
|
||||
set((s) => ({ isRightSidebarVisible: !s.isRightSidebarVisible })),
|
||||
|
||||
toggleRightMainPanelMode: (mode) => {
|
||||
const { rightMainPanelMode } = get();
|
||||
const isCurrentlyActive = rightMainPanelMode === mode;
|
||||
|
||||
if (isCurrentlyActive) {
|
||||
set({
|
||||
rightMainPanelMode: null,
|
||||
isLeftSidebarVisible: true,
|
||||
});
|
||||
} else {
|
||||
set({
|
||||
rightMainPanelMode: mode,
|
||||
isLeftSidebarVisible: isWideScreen()
|
||||
? get().isLeftSidebarVisible
|
||||
: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
setRightMainPanelMode: (mode) => {
|
||||
if (mode !== null) {
|
||||
set({
|
||||
rightMainPanelMode: mode,
|
||||
isLeftSidebarVisible: isWideScreen()
|
||||
? get().isLeftSidebarVisible
|
||||
: false,
|
||||
});
|
||||
} else {
|
||||
set({ rightMainPanelMode: null });
|
||||
}
|
||||
},
|
||||
|
||||
setLeftSidebarVisible: (value) => set({ isLeftSidebarVisible: value }),
|
||||
|
||||
setLeftMainPanelVisible: (value) =>
|
||||
set({ isLeftMainPanelVisible: value }),
|
||||
|
||||
triggerPreviewRefresh: () =>
|
||||
set((s) => ({ previewRefreshKey: s.previewRefreshKey + 1 })),
|
||||
|
||||
resetForCreateMode: () =>
|
||||
set({
|
||||
rightMainPanelMode: null,
|
||||
}),
|
||||
{ name: 'ui-preferences' }
|
||||
}),
|
||||
{
|
||||
name: 'ui-preferences',
|
||||
partialize: (state) => ({
|
||||
// UI preferences (all persisted)
|
||||
repoActions: state.repoActions,
|
||||
expanded: state.expanded,
|
||||
contextBarPosition: state.contextBarPosition,
|
||||
paneSizes: state.paneSizes,
|
||||
collapsedPaths: state.collapsedPaths,
|
||||
// Layout (only persist panel visibility, not mode states)
|
||||
isLeftSidebarVisible: state.isLeftSidebarVisible,
|
||||
isLeftMainPanelVisible: state.isLeftMainPanelVisible,
|
||||
isRightSidebarVisible: state.isRightSidebarVisible,
|
||||
}),
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
@@ -191,3 +300,7 @@ export function usePersistedCollapsedPaths(
|
||||
|
||||
return [pathSet, setPathSet];
|
||||
}
|
||||
|
||||
// Layout convenience hooks
|
||||
export const useIsRightMainPanelVisible = () =>
|
||||
useUiPreferencesStore((s) => s.rightMainPanelMode !== null);
|
||||
|
||||
@@ -366,6 +366,8 @@ pr_status: MergeStatus | null, };
|
||||
|
||||
export type WorkspaceSummaryResponse = { summaries: Array<WorkspaceSummary>, };
|
||||
|
||||
export type DiffStats = { files_changed: number, lines_added: number, lines_removed: number, };
|
||||
|
||||
export type DirectoryEntry = { name: string, path: string, is_directory: boolean, is_git_repo: boolean, last_modified: bigint | null, };
|
||||
|
||||
export type DirectoryListResponse = { entries: Array<DirectoryEntry>, current_path: string, };
|
||||
|
||||
Reference in New Issue
Block a user