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::WorkspaceSummaryRequest::decl(),
|
||||||
server::routes::task_attempts::workspace_summary::WorkspaceSummary::decl(),
|
server::routes::task_attempts::workspace_summary::WorkspaceSummary::decl(),
|
||||||
server::routes::task_attempts::workspace_summary::WorkspaceSummaryResponse::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::DirectoryEntry::decl(),
|
||||||
services::services::filesystem::DirectoryListResponse::decl(),
|
services::services::filesystem::DirectoryListResponse::decl(),
|
||||||
services::services::file_search::SearchMode::decl(),
|
services::services::file_search::SearchMode::decl(),
|
||||||
|
|||||||
@@ -56,11 +56,11 @@ pub struct WorkspaceSummaryResponse {
|
|||||||
pub summaries: Vec<WorkspaceSummary>,
|
pub summaries: Vec<WorkspaceSummary>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default, Serialize, TS)]
|
||||||
struct DiffStats {
|
pub struct DiffStats {
|
||||||
files_changed: usize,
|
pub files_changed: usize,
|
||||||
lines_added: usize,
|
pub lines_added: usize,
|
||||||
lines_removed: usize,
|
pub lines_removed: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch summary information for workspaces filtered by archived status.
|
/// Fetch summary information for workspaces filtered by archived status.
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ import {
|
|||||||
} from '@/stores/useUiPreferencesStore';
|
} from '@/stores/useUiPreferencesStore';
|
||||||
import DisplayConversationEntry from '@/components/NormalizedConversation/DisplayConversationEntry';
|
import DisplayConversationEntry from '@/components/NormalizedConversation/DisplayConversationEntry';
|
||||||
import { useMessageEditContext } from '@/contexts/MessageEditContext';
|
import { useMessageEditContext } from '@/contexts/MessageEditContext';
|
||||||
import { useFileNavigation } from '@/contexts/FileNavigationContext';
|
import { useChangesView } from '@/contexts/ChangesViewContext';
|
||||||
import { useLogNavigation } from '@/contexts/LogNavigationContext';
|
import { useLogsPanel } from '@/contexts/LogsPanelContext';
|
||||||
import { useWorkspaceContext } from '@/contexts/WorkspaceContext';
|
import { useWorkspaceContext } from '@/contexts/WorkspaceContext';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import {
|
import {
|
||||||
@@ -354,7 +354,7 @@ function FileEditEntry({
|
|||||||
expansionKey as PersistKey,
|
expansionKey as PersistKey,
|
||||||
pendingApproval
|
pendingApproval
|
||||||
);
|
);
|
||||||
const { viewFileInChanges, diffPaths } = useFileNavigation();
|
const { viewFileInChanges, diffPaths } = useChangesView();
|
||||||
|
|
||||||
// Calculate diff stats for edit changes
|
// Calculate diff stats for edit changes
|
||||||
const { additions, deletions } = useMemo(() => {
|
const { additions, deletions } = useMemo(() => {
|
||||||
@@ -561,7 +561,7 @@ function ToolSummaryEntry({
|
|||||||
`tool:${expansionKey}`,
|
`tool:${expansionKey}`,
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
const { viewToolContentInPanel } = useLogNavigation();
|
const { viewToolContentInPanel } = useLogsPanel();
|
||||||
const textRef = useRef<HTMLSpanElement>(null);
|
const textRef = useRef<HTMLSpanElement>(null);
|
||||||
const [isTruncated, setIsTruncated] = useState(false);
|
const [isTruncated, setIsTruncated] = useState(false);
|
||||||
|
|
||||||
|
|||||||
@@ -38,8 +38,11 @@ import {
|
|||||||
QuestionIcon,
|
QuestionIcon,
|
||||||
} from '@phosphor-icons/react';
|
} from '@phosphor-icons/react';
|
||||||
import { useDiffViewStore } from '@/stores/useDiffViewStore';
|
import { useDiffViewStore } from '@/stores/useDiffViewStore';
|
||||||
import { useUiPreferencesStore } from '@/stores/useUiPreferencesStore';
|
import {
|
||||||
import { useLayoutStore } from '@/stores/useLayoutStore';
|
useUiPreferencesStore,
|
||||||
|
RIGHT_MAIN_PANEL_MODES,
|
||||||
|
} from '@/stores/useUiPreferencesStore';
|
||||||
|
|
||||||
import { attemptsApi, tasksApi, repoApi } from '@/lib/api';
|
import { attemptsApi, tasksApi, repoApi } from '@/lib/api';
|
||||||
import { attemptKeys } from '@/hooks/useAttempt';
|
import { attemptKeys } from '@/hooks/useAttempt';
|
||||||
import { taskKeys } from '@/hooks/useTask';
|
import { taskKeys } from '@/hooks/useTask';
|
||||||
@@ -95,12 +98,12 @@ export interface ActionExecutorContext {
|
|||||||
// Context for evaluating action visibility and state conditions
|
// Context for evaluating action visibility and state conditions
|
||||||
export interface ActionVisibilityContext {
|
export interface ActionVisibilityContext {
|
||||||
// Layout state
|
// Layout state
|
||||||
isChangesMode: boolean;
|
rightMainPanelMode:
|
||||||
isLogsMode: boolean;
|
| (typeof RIGHT_MAIN_PANEL_MODES)[keyof typeof RIGHT_MAIN_PANEL_MODES]
|
||||||
isPreviewMode: boolean;
|
| null;
|
||||||
isSidebarVisible: boolean;
|
isLeftSidebarVisible: boolean;
|
||||||
isMainPanelVisible: boolean;
|
isLeftMainPanelVisible: boolean;
|
||||||
isGitPanelVisible: boolean;
|
isRightSidebarVisible: boolean;
|
||||||
isCreateMode: boolean;
|
isCreateMode: boolean;
|
||||||
|
|
||||||
// Workspace state
|
// Workspace state
|
||||||
@@ -415,7 +418,8 @@ export const Actions = {
|
|||||||
: 'Switch to Inline View',
|
: 'Switch to Inline View',
|
||||||
icon: ColumnsIcon,
|
icon: ColumnsIcon,
|
||||||
requiresTarget: false,
|
requiresTarget: false,
|
||||||
isVisible: (ctx) => ctx.isChangesMode,
|
isVisible: (ctx) =>
|
||||||
|
ctx.rightMainPanelMode === RIGHT_MAIN_PANEL_MODES.CHANGES,
|
||||||
isActive: (ctx) => ctx.diffViewMode === 'split',
|
isActive: (ctx) => ctx.diffViewMode === 'split',
|
||||||
getIcon: (ctx) => (ctx.diffViewMode === 'split' ? ColumnsIcon : RowsIcon),
|
getIcon: (ctx) => (ctx.diffViewMode === 'split' ? ColumnsIcon : RowsIcon),
|
||||||
getTooltip: (ctx) =>
|
getTooltip: (ctx) =>
|
||||||
@@ -433,7 +437,8 @@ export const Actions = {
|
|||||||
: 'Ignore Whitespace Changes',
|
: 'Ignore Whitespace Changes',
|
||||||
icon: EyeSlashIcon,
|
icon: EyeSlashIcon,
|
||||||
requiresTarget: false,
|
requiresTarget: false,
|
||||||
isVisible: (ctx) => ctx.isChangesMode,
|
isVisible: (ctx) =>
|
||||||
|
ctx.rightMainPanelMode === RIGHT_MAIN_PANEL_MODES.CHANGES,
|
||||||
execute: () => {
|
execute: () => {
|
||||||
const store = useDiffViewStore.getState();
|
const store = useDiffViewStore.getState();
|
||||||
store.setIgnoreWhitespace(!store.ignoreWhitespace);
|
store.setIgnoreWhitespace(!store.ignoreWhitespace);
|
||||||
@@ -448,7 +453,8 @@ export const Actions = {
|
|||||||
: 'Enable Line Wrapping',
|
: 'Enable Line Wrapping',
|
||||||
icon: TextAlignLeftIcon,
|
icon: TextAlignLeftIcon,
|
||||||
requiresTarget: false,
|
requiresTarget: false,
|
||||||
isVisible: (ctx) => ctx.isChangesMode,
|
isVisible: (ctx) =>
|
||||||
|
ctx.rightMainPanelMode === RIGHT_MAIN_PANEL_MODES.CHANGES,
|
||||||
execute: () => {
|
execute: () => {
|
||||||
const store = useDiffViewStore.getState();
|
const store = useDiffViewStore.getState();
|
||||||
store.setWrapText(!store.wrapText);
|
store.setWrapText(!store.wrapText);
|
||||||
@@ -456,94 +462,106 @@ export const Actions = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// === Layout Panel Actions ===
|
// === Layout Panel Actions ===
|
||||||
ToggleSidebar: {
|
ToggleLeftSidebar: {
|
||||||
id: 'toggle-sidebar',
|
id: 'toggle-left-sidebar',
|
||||||
label: () =>
|
label: () =>
|
||||||
useLayoutStore.getState().isSidebarVisible
|
useUiPreferencesStore.getState().isLeftSidebarVisible
|
||||||
? 'Hide Sidebar'
|
? 'Hide Left Sidebar'
|
||||||
: 'Show Sidebar',
|
: 'Show Left Sidebar',
|
||||||
icon: SidebarSimpleIcon,
|
icon: SidebarSimpleIcon,
|
||||||
requiresTarget: false,
|
requiresTarget: false,
|
||||||
isActive: (ctx) => ctx.isSidebarVisible,
|
isActive: (ctx) => ctx.isLeftSidebarVisible,
|
||||||
execute: () => {
|
execute: () => {
|
||||||
useLayoutStore.getState().toggleSidebar();
|
useUiPreferencesStore.getState().toggleLeftSidebar();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
ToggleMainPanel: {
|
ToggleLeftMainPanel: {
|
||||||
id: 'toggle-main-panel',
|
id: 'toggle-left-main-panel',
|
||||||
label: () =>
|
label: () =>
|
||||||
useLayoutStore.getState().isMainPanelVisible
|
useUiPreferencesStore.getState().isLeftMainPanelVisible
|
||||||
? 'Hide Chat Panel'
|
? 'Hide Chat Panel'
|
||||||
: 'Show Chat Panel',
|
: 'Show Chat Panel',
|
||||||
icon: ChatsTeardropIcon,
|
icon: ChatsTeardropIcon,
|
||||||
requiresTarget: false,
|
requiresTarget: false,
|
||||||
isActive: (ctx) => ctx.isMainPanelVisible,
|
isActive: (ctx) => ctx.isLeftMainPanelVisible,
|
||||||
isEnabled: (ctx) => !(ctx.isMainPanelVisible && !ctx.isChangesMode),
|
isEnabled: (ctx) =>
|
||||||
|
!(ctx.isLeftMainPanelVisible && ctx.rightMainPanelMode === null),
|
||||||
execute: () => {
|
execute: () => {
|
||||||
useLayoutStore.getState().toggleMainPanel();
|
useUiPreferencesStore.getState().toggleLeftMainPanel();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
ToggleGitPanel: {
|
ToggleRightSidebar: {
|
||||||
id: 'toggle-git-panel',
|
id: 'toggle-right-sidebar',
|
||||||
label: () =>
|
label: () =>
|
||||||
useLayoutStore.getState().isGitPanelVisible
|
useUiPreferencesStore.getState().isRightSidebarVisible
|
||||||
? 'Hide Git Panel'
|
? 'Hide Right Sidebar'
|
||||||
: 'Show Git Panel',
|
: 'Show Right Sidebar',
|
||||||
icon: RightSidebarIcon,
|
icon: RightSidebarIcon,
|
||||||
requiresTarget: false,
|
requiresTarget: false,
|
||||||
isActive: (ctx) => ctx.isGitPanelVisible,
|
isActive: (ctx) => ctx.isRightSidebarVisible,
|
||||||
execute: () => {
|
execute: () => {
|
||||||
useLayoutStore.getState().toggleGitPanel();
|
useUiPreferencesStore.getState().toggleRightSidebar();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
ToggleChangesMode: {
|
ToggleChangesMode: {
|
||||||
id: 'toggle-changes-mode',
|
id: 'toggle-changes-mode',
|
||||||
label: () =>
|
label: () =>
|
||||||
useLayoutStore.getState().isChangesMode
|
useUiPreferencesStore.getState().rightMainPanelMode ===
|
||||||
|
RIGHT_MAIN_PANEL_MODES.CHANGES
|
||||||
? 'Hide Changes Panel'
|
? 'Hide Changes Panel'
|
||||||
: 'Show Changes Panel',
|
: 'Show Changes Panel',
|
||||||
icon: GitDiffIcon,
|
icon: GitDiffIcon,
|
||||||
requiresTarget: false,
|
requiresTarget: false,
|
||||||
isVisible: (ctx) => !ctx.isCreateMode,
|
isVisible: (ctx) => !ctx.isCreateMode,
|
||||||
isActive: (ctx) => ctx.isChangesMode,
|
isActive: (ctx) =>
|
||||||
|
ctx.rightMainPanelMode === RIGHT_MAIN_PANEL_MODES.CHANGES,
|
||||||
isEnabled: (ctx) => !ctx.isCreateMode,
|
isEnabled: (ctx) => !ctx.isCreateMode,
|
||||||
execute: () => {
|
execute: () => {
|
||||||
useLayoutStore.getState().toggleChangesMode();
|
useUiPreferencesStore
|
||||||
|
.getState()
|
||||||
|
.toggleRightMainPanelMode(RIGHT_MAIN_PANEL_MODES.CHANGES);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
ToggleLogsMode: {
|
ToggleLogsMode: {
|
||||||
id: 'toggle-logs-mode',
|
id: 'toggle-logs-mode',
|
||||||
label: () =>
|
label: () =>
|
||||||
useLayoutStore.getState().isLogsMode
|
useUiPreferencesStore.getState().rightMainPanelMode ===
|
||||||
|
RIGHT_MAIN_PANEL_MODES.LOGS
|
||||||
? 'Hide Logs Panel'
|
? 'Hide Logs Panel'
|
||||||
: 'Show Logs Panel',
|
: 'Show Logs Panel',
|
||||||
icon: TerminalIcon,
|
icon: TerminalIcon,
|
||||||
requiresTarget: false,
|
requiresTarget: false,
|
||||||
isVisible: (ctx) => !ctx.isCreateMode,
|
isVisible: (ctx) => !ctx.isCreateMode,
|
||||||
isActive: (ctx) => ctx.isLogsMode,
|
isActive: (ctx) => ctx.rightMainPanelMode === RIGHT_MAIN_PANEL_MODES.LOGS,
|
||||||
isEnabled: (ctx) => !ctx.isCreateMode,
|
isEnabled: (ctx) => !ctx.isCreateMode,
|
||||||
execute: () => {
|
execute: () => {
|
||||||
useLayoutStore.getState().toggleLogsMode();
|
useUiPreferencesStore
|
||||||
|
.getState()
|
||||||
|
.toggleRightMainPanelMode(RIGHT_MAIN_PANEL_MODES.LOGS);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
TogglePreviewMode: {
|
TogglePreviewMode: {
|
||||||
id: 'toggle-preview-mode',
|
id: 'toggle-preview-mode',
|
||||||
label: () =>
|
label: () =>
|
||||||
useLayoutStore.getState().isPreviewMode
|
useUiPreferencesStore.getState().rightMainPanelMode ===
|
||||||
|
RIGHT_MAIN_PANEL_MODES.PREVIEW
|
||||||
? 'Hide Preview Panel'
|
? 'Hide Preview Panel'
|
||||||
: 'Show Preview Panel',
|
: 'Show Preview Panel',
|
||||||
icon: DesktopIcon,
|
icon: DesktopIcon,
|
||||||
requiresTarget: false,
|
requiresTarget: false,
|
||||||
isVisible: (ctx) => !ctx.isCreateMode,
|
isVisible: (ctx) => !ctx.isCreateMode,
|
||||||
isActive: (ctx) => ctx.isPreviewMode,
|
isActive: (ctx) =>
|
||||||
|
ctx.rightMainPanelMode === RIGHT_MAIN_PANEL_MODES.PREVIEW,
|
||||||
isEnabled: (ctx) => !ctx.isCreateMode,
|
isEnabled: (ctx) => !ctx.isCreateMode,
|
||||||
execute: () => {
|
execute: () => {
|
||||||
useLayoutStore.getState().togglePreviewMode();
|
useUiPreferencesStore
|
||||||
|
.getState()
|
||||||
|
.toggleRightMainPanelMode(RIGHT_MAIN_PANEL_MODES.PREVIEW);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -592,7 +610,8 @@ export const Actions = {
|
|||||||
},
|
},
|
||||||
icon: CaretDoubleUpIcon,
|
icon: CaretDoubleUpIcon,
|
||||||
requiresTarget: false,
|
requiresTarget: false,
|
||||||
isVisible: (ctx) => ctx.isChangesMode,
|
isVisible: (ctx) =>
|
||||||
|
ctx.rightMainPanelMode === RIGHT_MAIN_PANEL_MODES.CHANGES,
|
||||||
getIcon: (ctx) =>
|
getIcon: (ctx) =>
|
||||||
ctx.isAllDiffsExpanded ? CaretDoubleUpIcon : CaretDoubleDownIcon,
|
ctx.isAllDiffsExpanded ? CaretDoubleUpIcon : CaretDoubleDownIcon,
|
||||||
getTooltip: (ctx) =>
|
getTooltip: (ctx) =>
|
||||||
@@ -686,7 +705,9 @@ export const Actions = {
|
|||||||
} else {
|
} else {
|
||||||
ctx.startDevServer();
|
ctx.startDevServer();
|
||||||
// Auto-open preview mode when starting dev server
|
// 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.ToggleDiffViewMode,
|
||||||
Actions.ToggleAllDiffs,
|
Actions.ToggleAllDiffs,
|
||||||
NavbarDivider,
|
NavbarDivider,
|
||||||
Actions.ToggleSidebar,
|
Actions.ToggleLeftSidebar,
|
||||||
Actions.ToggleMainPanel,
|
Actions.ToggleLeftMainPanel,
|
||||||
Actions.ToggleChangesMode,
|
Actions.ToggleChangesMode,
|
||||||
Actions.ToggleLogsMode,
|
Actions.ToggleLogsMode,
|
||||||
Actions.TogglePreviewMode,
|
Actions.TogglePreviewMode,
|
||||||
Actions.ToggleGitPanel,
|
Actions.ToggleRightSidebar,
|
||||||
NavbarDivider,
|
NavbarDivider,
|
||||||
Actions.OpenCommandBar,
|
Actions.OpenCommandBar,
|
||||||
Actions.Feedback,
|
Actions.Feedback,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Icon } from '@phosphor-icons/react';
|
import type { Icon } from '@phosphor-icons/react';
|
||||||
import { type ActionDefinition, type ActionVisibilityContext } from './index';
|
import { type ActionDefinition, type ActionVisibilityContext } from './index';
|
||||||
import { Actions } from './index';
|
import { Actions } from './index';
|
||||||
|
import { RIGHT_MAIN_PANEL_MODES } from '@/stores/useUiPreferencesStore';
|
||||||
|
|
||||||
// Define page IDs first to avoid circular reference
|
// Define page IDs first to avoid circular reference
|
||||||
export type PageId =
|
export type PageId =
|
||||||
@@ -130,7 +131,8 @@ export const Pages: Record<StaticPageId, CommandBarPage> = {
|
|||||||
id: 'diff-options',
|
id: 'diff-options',
|
||||||
title: 'Diff Options',
|
title: 'Diff Options',
|
||||||
parent: 'root',
|
parent: 'root',
|
||||||
isVisible: (ctx) => ctx.isChangesMode,
|
isVisible: (ctx) =>
|
||||||
|
ctx.rightMainPanelMode === RIGHT_MAIN_PANEL_MODES.CHANGES,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
type: 'group',
|
type: 'group',
|
||||||
@@ -155,9 +157,9 @@ export const Pages: Record<StaticPageId, CommandBarPage> = {
|
|||||||
type: 'group',
|
type: 'group',
|
||||||
label: 'Panels',
|
label: 'Panels',
|
||||||
items: [
|
items: [
|
||||||
{ type: 'action', action: Actions.ToggleSidebar },
|
{ type: 'action', action: Actions.ToggleLeftSidebar },
|
||||||
{ type: 'action', action: Actions.ToggleMainPanel },
|
{ type: 'action', action: Actions.ToggleLeftMainPanel },
|
||||||
{ type: 'action', action: Actions.ToggleGitPanel },
|
{ type: 'action', action: Actions.ToggleRightSidebar },
|
||||||
{ type: 'action', action: Actions.ToggleChangesMode },
|
{ type: 'action', action: Actions.ToggleChangesMode },
|
||||||
{ type: 'action', action: Actions.ToggleLogsMode },
|
{ type: 'action', action: Actions.ToggleLogsMode },
|
||||||
{ type: 'action', action: Actions.TogglePreviewMode },
|
{ type: 'action', action: Actions.TogglePreviewMode },
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useLayoutStore } from '@/stores/useLayoutStore';
|
|
||||||
import { useDiffViewStore, useDiffViewMode } from '@/stores/useDiffViewStore';
|
|
||||||
import { useUiPreferencesStore } from '@/stores/useUiPreferencesStore';
|
import { useUiPreferencesStore } from '@/stores/useUiPreferencesStore';
|
||||||
|
import { useDiffViewStore, useDiffViewMode } from '@/stores/useDiffViewStore';
|
||||||
import { useWorkspaceContext } from '@/contexts/WorkspaceContext';
|
import { useWorkspaceContext } from '@/contexts/WorkspaceContext';
|
||||||
import { useUserSystem } from '@/components/ConfigProvider';
|
import { useUserSystem } from '@/components/ConfigProvider';
|
||||||
import { useDevServer } from '@/hooks/useDevServer';
|
import { useDevServer } from '@/hooks/useDevServer';
|
||||||
@@ -23,7 +22,7 @@ import type { CommandBarPage } from './pages';
|
|||||||
* action visibility and state conditions.
|
* action visibility and state conditions.
|
||||||
*/
|
*/
|
||||||
export function useActionVisibilityContext(): ActionVisibilityContext {
|
export function useActionVisibilityContext(): ActionVisibilityContext {
|
||||||
const layout = useLayoutStore();
|
const layout = useUiPreferencesStore();
|
||||||
const { workspace, workspaceId, isCreateMode, repos } = useWorkspaceContext();
|
const { workspace, workspaceId, isCreateMode, repos } = useWorkspaceContext();
|
||||||
const diffPaths = useDiffViewStore((s) => s.diffPaths);
|
const diffPaths = useDiffViewStore((s) => s.diffPaths);
|
||||||
const diffViewMode = useDiffViewMode();
|
const diffViewMode = useDiffViewMode();
|
||||||
@@ -62,12 +61,10 @@ export function useActionVisibilityContext(): ActionVisibilityContext {
|
|||||||
false;
|
false;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isChangesMode: layout.isChangesMode,
|
rightMainPanelMode: layout.rightMainPanelMode,
|
||||||
isLogsMode: layout.isLogsMode,
|
isLeftSidebarVisible: layout.isLeftSidebarVisible,
|
||||||
isPreviewMode: layout.isPreviewMode,
|
isLeftMainPanelVisible: layout.isLeftMainPanelVisible,
|
||||||
isSidebarVisible: layout.isSidebarVisible,
|
isRightSidebarVisible: layout.isRightSidebarVisible,
|
||||||
isMainPanelVisible: layout.isMainPanelVisible,
|
|
||||||
isGitPanelVisible: layout.isGitPanelVisible,
|
|
||||||
isCreateMode,
|
isCreateMode,
|
||||||
hasWorkspace: !!workspace,
|
hasWorkspace: !!workspace,
|
||||||
workspaceArchived: workspace?.archived ?? false,
|
workspaceArchived: workspace?.archived ?? false,
|
||||||
@@ -84,12 +81,10 @@ export function useActionVisibilityContext(): ActionVisibilityContext {
|
|||||||
isAttemptRunning: isAttemptRunningVisible,
|
isAttemptRunning: isAttemptRunningVisible,
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
layout.isChangesMode,
|
layout.rightMainPanelMode,
|
||||||
layout.isLogsMode,
|
layout.isLeftSidebarVisible,
|
||||||
layout.isPreviewMode,
|
layout.isLeftMainPanelVisible,
|
||||||
layout.isSidebarVisible,
|
layout.isRightSidebarVisible,
|
||||||
layout.isMainPanelVisible,
|
|
||||||
layout.isGitPanelVisible,
|
|
||||||
isCreateMode,
|
isCreateMode,
|
||||||
workspace,
|
workspace,
|
||||||
repos,
|
repos,
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ import {
|
|||||||
} from 'react';
|
} from 'react';
|
||||||
import { ChangesPanel } from '../views/ChangesPanel';
|
import { ChangesPanel } from '../views/ChangesPanel';
|
||||||
import { sortDiffs } from '@/utils/fileTreeUtils';
|
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';
|
import type { Diff, DiffChangeKind } from 'shared/types';
|
||||||
|
|
||||||
// Auto-collapse defaults based on change type (matches DiffsPanel behavior)
|
// Auto-collapse defaults based on change type (matches DiffsPanel behavior)
|
||||||
@@ -128,24 +131,20 @@ function useInViewObserver(
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ChangesPanelContainerProps {
|
interface ChangesPanelContainerProps {
|
||||||
diffs: Diff[];
|
|
||||||
selectedFilePath?: string | null;
|
|
||||||
onFileInViewChange?: (path: string) => void;
|
|
||||||
className?: string;
|
className?: string;
|
||||||
/** Project ID for @ mentions in comments */
|
|
||||||
projectId?: string;
|
|
||||||
/** Attempt ID for opening files in IDE */
|
/** Attempt ID for opening files in IDE */
|
||||||
attemptId?: string;
|
attemptId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChangesPanelContainer({
|
export function ChangesPanelContainer({
|
||||||
diffs,
|
|
||||||
selectedFilePath,
|
|
||||||
onFileInViewChange,
|
|
||||||
className,
|
className,
|
||||||
projectId,
|
|
||||||
attemptId,
|
attemptId,
|
||||||
}: ChangesPanelContainerProps) {
|
}: 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 diffRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
// Track which diffs we've processed for auto-collapse
|
// Track which diffs we've processed for auto-collapse
|
||||||
@@ -155,7 +154,7 @@ export function ChangesPanelContainer({
|
|||||||
const observeElement = useInViewObserver(
|
const observeElement = useInViewObserver(
|
||||||
diffRefs,
|
diffRefs,
|
||||||
containerRef,
|
containerRef,
|
||||||
onFileInViewChange
|
setFileInView
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -208,7 +207,7 @@ export function ChangesPanelContainer({
|
|||||||
className={className}
|
className={className}
|
||||||
diffItems={diffItems}
|
diffItems={diffItems}
|
||||||
onDiffRef={handleDiffRef}
|
onDiffRef={handleDiffRef}
|
||||||
projectId={projectId}
|
projectId={task?.project_id}
|
||||||
attemptId={attemptId}
|
attemptId={attemptId}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ import {
|
|||||||
} from '@/utils/fileTreeUtils';
|
} from '@/utils/fileTreeUtils';
|
||||||
import { usePersistedCollapsedPaths } from '@/stores/useUiPreferencesStore';
|
import { usePersistedCollapsedPaths } from '@/stores/useUiPreferencesStore';
|
||||||
import { useWorkspaceContext } from '@/contexts/WorkspaceContext';
|
import { useWorkspaceContext } from '@/contexts/WorkspaceContext';
|
||||||
|
import { useChangesView } from '@/contexts/ChangesViewContext';
|
||||||
import type { Diff } from 'shared/types';
|
import type { Diff } from 'shared/types';
|
||||||
|
|
||||||
interface FileTreeContainerProps {
|
interface FileTreeContainerProps {
|
||||||
workspaceId?: string;
|
workspaceId?: string;
|
||||||
diffs: Diff[];
|
diffs: Diff[];
|
||||||
selectedFilePath?: string | null;
|
|
||||||
onSelectFile?: (path: string, diff: Diff) => void;
|
onSelectFile?: (path: string, diff: Diff) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
@@ -21,10 +21,10 @@ interface FileTreeContainerProps {
|
|||||||
export function FileTreeContainer({
|
export function FileTreeContainer({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
diffs,
|
diffs,
|
||||||
selectedFilePath,
|
|
||||||
onSelectFile,
|
onSelectFile,
|
||||||
className,
|
className,
|
||||||
}: FileTreeContainerProps) {
|
}: FileTreeContainerProps) {
|
||||||
|
const { fileInView } = useChangesView();
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [collapsedPaths, setCollapsedPaths] =
|
const [collapsedPaths, setCollapsedPaths] =
|
||||||
usePersistedCollapsedPaths(workspaceId);
|
usePersistedCollapsedPaths(workspaceId);
|
||||||
@@ -39,19 +39,19 @@ export function FileTreeContainer({
|
|||||||
isGitHubCommentsLoading,
|
isGitHubCommentsLoading,
|
||||||
} = useWorkspaceContext();
|
} = useWorkspaceContext();
|
||||||
|
|
||||||
// Sync selectedPath with external selectedFilePath prop and scroll into view
|
// Sync selectedPath with fileInView from context and scroll into view
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedFilePath !== undefined) {
|
if (fileInView !== undefined) {
|
||||||
setSelectedPath(selectedFilePath);
|
setSelectedPath(fileInView);
|
||||||
// Scroll the selected node into view if needed
|
// Scroll the selected node into view if needed
|
||||||
if (selectedFilePath) {
|
if (fileInView) {
|
||||||
const el = nodeRefs.current.get(selectedFilePath);
|
const el = nodeRefs.current.get(fileInView);
|
||||||
if (el) {
|
if (el) {
|
||||||
el.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
el.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [selectedFilePath]);
|
}, [fileInView]);
|
||||||
|
|
||||||
const handleNodeRef = useCallback(
|
const handleNodeRef = useCallback(
|
||||||
(path: string, el: HTMLDivElement | null) => {
|
(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,
|
type LogEntry,
|
||||||
} from '../VirtualizedProcessLogs';
|
} from '../VirtualizedProcessLogs';
|
||||||
import { useLogStream } from '@/hooks/useLogStream';
|
import { useLogStream } from '@/hooks/useLogStream';
|
||||||
|
import { useLogsPanel } from '@/contexts/LogsPanelContext';
|
||||||
|
|
||||||
export type LogsPanelContent =
|
export type LogsPanelContent =
|
||||||
| { type: 'process'; processId: string }
|
| { type: 'process'; processId: string }
|
||||||
| { type: 'tool'; toolName: string; content: string; command?: string };
|
| { type: 'tool'; toolName: string; content: string; command?: string };
|
||||||
|
|
||||||
interface LogsContentContainerProps {
|
interface LogsContentContainerProps {
|
||||||
content: LogsPanelContent | null;
|
|
||||||
className?: string;
|
className?: string;
|
||||||
searchQuery?: string;
|
|
||||||
currentMatchIndex?: number;
|
|
||||||
onMatchIndicesChange?: (indices: number[]) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LogsContentContainer({
|
export function LogsContentContainer({ className }: LogsContentContainerProps) {
|
||||||
content,
|
const {
|
||||||
className,
|
logsPanelContent: content,
|
||||||
searchQuery = '',
|
logSearchQuery: searchQuery,
|
||||||
currentMatchIndex = 0,
|
logCurrentMatchIdx: currentMatchIndex,
|
||||||
onMatchIndicesChange,
|
setLogMatchIndices: onMatchIndicesChange,
|
||||||
}: LogsContentContainerProps) {
|
} = useLogsPanel();
|
||||||
const { t } = useTranslation('common');
|
const { t } = useTranslation('common');
|
||||||
// Get logs for process content (only when type is 'process')
|
// Get logs for process content (only when type is 'process')
|
||||||
const processId = content?.type === 'process' ? content.processId : '';
|
const processId = content?.type === 'process' ? content.processId : '';
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
type ScreenSize,
|
type ScreenSize,
|
||||||
} from '@/hooks/usePreviewSettings';
|
} from '@/hooks/usePreviewSettings';
|
||||||
import { useLogStream } from '@/hooks/useLogStream';
|
import { useLogStream } from '@/hooks/useLogStream';
|
||||||
import { useLayoutStore } from '@/stores/useLayoutStore';
|
import { useUiPreferencesStore } from '@/stores/useUiPreferencesStore';
|
||||||
import { useWorkspaceContext } from '@/contexts/WorkspaceContext';
|
import { useWorkspaceContext } from '@/contexts/WorkspaceContext';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { ScriptFixerDialog } from '@/components/dialogs/scripts/ScriptFixerDialog';
|
import { ScriptFixerDialog } from '@/components/dialogs/scripts/ScriptFixerDialog';
|
||||||
@@ -25,8 +25,10 @@ export function PreviewBrowserContainer({
|
|||||||
className,
|
className,
|
||||||
}: PreviewBrowserContainerProps) {
|
}: PreviewBrowserContainerProps) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const previewRefreshKey = useLayoutStore((s) => s.previewRefreshKey);
|
const previewRefreshKey = useUiPreferencesStore((s) => s.previewRefreshKey);
|
||||||
const triggerPreviewRefresh = useLayoutStore((s) => s.triggerPreviewRefresh);
|
const triggerPreviewRefresh = useUiPreferencesStore(
|
||||||
|
(s) => s.triggerPreviewRefresh
|
||||||
|
);
|
||||||
const { repos, workspaceId } = useWorkspaceContext();
|
const { repos, workspaceId } = useWorkspaceContext();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ import { useCallback, useState, useEffect } from 'react';
|
|||||||
import { PreviewControls } from '../views/PreviewControls';
|
import { PreviewControls } from '../views/PreviewControls';
|
||||||
import { usePreviewDevServer } from '../hooks/usePreviewDevServer';
|
import { usePreviewDevServer } from '../hooks/usePreviewDevServer';
|
||||||
import { useLogStream } from '@/hooks/useLogStream';
|
import { useLogStream } from '@/hooks/useLogStream';
|
||||||
import { useLayoutStore } from '@/stores/useLayoutStore';
|
import {
|
||||||
|
useUiPreferencesStore,
|
||||||
|
RIGHT_MAIN_PANEL_MODES,
|
||||||
|
} from '@/stores/useUiPreferencesStore';
|
||||||
import { useWorkspaceContext } from '@/contexts/WorkspaceContext';
|
import { useWorkspaceContext } from '@/contexts/WorkspaceContext';
|
||||||
|
|
||||||
interface PreviewControlsContainerProps {
|
interface PreviewControlsContainerProps {
|
||||||
@@ -17,7 +20,9 @@ export function PreviewControlsContainer({
|
|||||||
className,
|
className,
|
||||||
}: PreviewControlsContainerProps) {
|
}: PreviewControlsContainerProps) {
|
||||||
const { repos } = useWorkspaceContext();
|
const { repos } = useWorkspaceContext();
|
||||||
const setLogsMode = useLayoutStore((s) => s.setLogsMode);
|
const setRightMainPanelMode = useUiPreferencesStore(
|
||||||
|
(s) => s.setRightMainPanelMode
|
||||||
|
);
|
||||||
|
|
||||||
const { isStarting, runningDevServers, devServerProcesses } =
|
const { isStarting, runningDevServers, devServerProcesses } =
|
||||||
usePreviewDevServer(attemptId);
|
usePreviewDevServer(attemptId);
|
||||||
@@ -42,10 +47,10 @@ export function PreviewControlsContainer({
|
|||||||
if (targetId && onViewProcessInPanel) {
|
if (targetId && onViewProcessInPanel) {
|
||||||
onViewProcessInPanel(targetId);
|
onViewProcessInPanel(targetId);
|
||||||
} else {
|
} else {
|
||||||
setLogsMode(true);
|
setRightMainPanelMode(RIGHT_MAIN_PANEL_MODES.LOGS);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[activeProcess?.id, onViewProcessInPanel, setLogsMode]
|
[activeProcess?.id, onViewProcessInPanel, setRightMainPanelMode]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleTabChange = useCallback((processId: string) => {
|
const handleTabChange = useCallback((processId: string) => {
|
||||||
|
|||||||
@@ -1,36 +1,29 @@
|
|||||||
import { useEffect, useMemo, useCallback } from 'react';
|
import { useEffect, useMemo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useExecutionProcessesContext } from '@/contexts/ExecutionProcessesContext';
|
import { useExecutionProcessesContext } from '@/contexts/ExecutionProcessesContext';
|
||||||
|
import { useLogsPanel } from '@/contexts/LogsPanelContext';
|
||||||
import { ProcessListItem } from '../primitives/ProcessListItem';
|
import { ProcessListItem } from '../primitives/ProcessListItem';
|
||||||
import { CollapsibleSectionHeader } from '../primitives/CollapsibleSectionHeader';
|
import { CollapsibleSectionHeader } from '../primitives/CollapsibleSectionHeader';
|
||||||
import { InputField } from '../primitives/InputField';
|
import { InputField } from '../primitives/InputField';
|
||||||
import { CaretUpIcon, CaretDownIcon } from '@phosphor-icons/react';
|
import { CaretUpIcon, CaretDownIcon } from '@phosphor-icons/react';
|
||||||
import { PERSIST_KEYS } from '@/stores/useUiPreferencesStore';
|
import { PERSIST_KEYS } from '@/stores/useUiPreferencesStore';
|
||||||
|
|
||||||
interface ProcessListContainerProps {
|
export function ProcessListContainer() {
|
||||||
selectedProcessId: string | null;
|
const {
|
||||||
onSelectProcess: (processId: string) => void;
|
logsPanelContent,
|
||||||
disableAutoSelect?: boolean;
|
logSearchQuery: searchQuery,
|
||||||
// Search props
|
logMatchIndices,
|
||||||
searchQuery?: string;
|
logCurrentMatchIdx: currentMatchIdx,
|
||||||
onSearchQueryChange?: (query: string) => void;
|
setLogSearchQuery: onSearchQueryChange,
|
||||||
matchCount?: number;
|
handleLogPrevMatch: onPrevMatch,
|
||||||
currentMatchIdx?: number;
|
handleLogNextMatch: onNextMatch,
|
||||||
onPrevMatch?: () => void;
|
viewProcessInPanel: onSelectProcess,
|
||||||
onNextMatch?: () => void;
|
} = useLogsPanel();
|
||||||
}
|
|
||||||
|
|
||||||
export function ProcessListContainer({
|
const selectedProcessId =
|
||||||
selectedProcessId,
|
logsPanelContent?.type === 'process' ? logsPanelContent.processId : null;
|
||||||
onSelectProcess,
|
const disableAutoSelect = logsPanelContent?.type === 'tool';
|
||||||
disableAutoSelect,
|
const matchCount = logMatchIndices.length;
|
||||||
searchQuery = '',
|
|
||||||
onSearchQueryChange,
|
|
||||||
matchCount = 0,
|
|
||||||
currentMatchIdx = 0,
|
|
||||||
onPrevMatch,
|
|
||||||
onNextMatch,
|
|
||||||
}: ProcessListContainerProps) {
|
|
||||||
const { t } = useTranslation('common');
|
const { t } = useTranslation('common');
|
||||||
const { executionProcessesVisible } = useExecutionProcessesContext();
|
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,
|
SessionChatBox,
|
||||||
type ExecutionStatus,
|
type ExecutionStatus,
|
||||||
} from '../primitives/SessionChatBox';
|
} from '../primitives/SessionChatBox';
|
||||||
|
import {
|
||||||
|
useUiPreferencesStore,
|
||||||
|
RIGHT_MAIN_PANEL_MODES,
|
||||||
|
} from '@/stores/useUiPreferencesStore';
|
||||||
import { Actions, type ActionDefinition } from '../actions';
|
import { Actions, type ActionDefinition } from '../actions';
|
||||||
import {
|
import {
|
||||||
isActionVisible,
|
isActionVisible,
|
||||||
@@ -65,8 +69,6 @@ interface SessionChatBoxContainerProps {
|
|||||||
linesAdded?: number;
|
linesAdded?: number;
|
||||||
/** Number of lines removed */
|
/** Number of lines removed */
|
||||||
linesRemoved?: number;
|
linesRemoved?: number;
|
||||||
/** Callback to view code changes (toggle ChangesPanel) */
|
|
||||||
onViewCode?: () => void;
|
|
||||||
/** Available sessions for this workspace */
|
/** Available sessions for this workspace */
|
||||||
sessions?: Session[];
|
sessions?: Session[];
|
||||||
/** Called when a session is selected */
|
/** Called when a session is selected */
|
||||||
@@ -87,7 +89,6 @@ export function SessionChatBoxContainer({
|
|||||||
filesChanged,
|
filesChanged,
|
||||||
linesAdded,
|
linesAdded,
|
||||||
linesRemoved,
|
linesRemoved,
|
||||||
onViewCode,
|
|
||||||
sessions = [],
|
sessions = [],
|
||||||
onSelectSession,
|
onSelectSession,
|
||||||
projectId,
|
projectId,
|
||||||
@@ -101,6 +102,15 @@ export function SessionChatBoxContainer({
|
|||||||
|
|
||||||
const { executeAction } = useActions();
|
const { executeAction } = useActions();
|
||||||
const actionCtx = useActionVisibilityContext();
|
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
|
// Get entries early to extract pending approval for scratch key
|
||||||
const { entries } = useEntries();
|
const { entries } = useEntries();
|
||||||
@@ -578,8 +588,8 @@ export function SessionChatBoxContainer({
|
|||||||
filesChanged: 0,
|
filesChanged: 0,
|
||||||
linesAdded: 0,
|
linesAdded: 0,
|
||||||
linesRemoved: 0,
|
linesRemoved: 0,
|
||||||
onViewCode: undefined,
|
|
||||||
}}
|
}}
|
||||||
|
onViewCode={handleViewCode}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -587,6 +597,7 @@ export function SessionChatBoxContainer({
|
|||||||
return (
|
return (
|
||||||
<SessionChatBox
|
<SessionChatBox
|
||||||
status={status}
|
status={status}
|
||||||
|
onViewCode={handleViewCode}
|
||||||
workspaceId={workspaceId}
|
workspaceId={workspaceId}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
editor={{
|
editor={{
|
||||||
@@ -621,7 +632,6 @@ export function SessionChatBoxContainer({
|
|||||||
filesChanged,
|
filesChanged,
|
||||||
linesAdded,
|
linesAdded,
|
||||||
linesRemoved,
|
linesRemoved,
|
||||||
onViewCode,
|
|
||||||
hasConflicts,
|
hasConflicts,
|
||||||
conflictedFilesCount,
|
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 { createWorkspaceWithSession } from '@/types/attempt';
|
||||||
import { WorkspacesMain } from '@/components/ui-new/views/WorkspacesMain';
|
import { WorkspacesMain } from '@/components/ui-new/views/WorkspacesMain';
|
||||||
import { useTask } from '@/hooks/useTask';
|
import { useTask } from '@/hooks/useTask';
|
||||||
|
import { useWorkspaceContext } from '@/contexts/WorkspaceContext';
|
||||||
interface DiffStats {
|
|
||||||
filesChanged: number;
|
|
||||||
linesAdded: number;
|
|
||||||
linesRemoved: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface WorkspacesMainContainerProps {
|
interface WorkspacesMainContainerProps {
|
||||||
selectedWorkspace: Workspace | null;
|
selectedWorkspace: Workspace | null;
|
||||||
@@ -20,10 +15,6 @@ interface WorkspacesMainContainerProps {
|
|||||||
isNewSessionMode?: boolean;
|
isNewSessionMode?: boolean;
|
||||||
/** Callback to start new session mode */
|
/** Callback to start new session mode */
|
||||||
onStartNewSession?: () => void;
|
onStartNewSession?: () => void;
|
||||||
/** Callback to toggle changes panel */
|
|
||||||
onViewCode?: () => void;
|
|
||||||
/** Diff statistics from the workspace */
|
|
||||||
diffStats?: DiffStats;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WorkspacesMainContainer({
|
export function WorkspacesMainContainer({
|
||||||
@@ -34,9 +25,8 @@ export function WorkspacesMainContainer({
|
|||||||
isLoading,
|
isLoading,
|
||||||
isNewSessionMode,
|
isNewSessionMode,
|
||||||
onStartNewSession,
|
onStartNewSession,
|
||||||
onViewCode,
|
|
||||||
diffStats,
|
|
||||||
}: WorkspacesMainContainerProps) {
|
}: WorkspacesMainContainerProps) {
|
||||||
|
const { diffStats } = useWorkspaceContext();
|
||||||
const containerRef = useRef<HTMLElement>(null);
|
const containerRef = useRef<HTMLElement>(null);
|
||||||
|
|
||||||
// Fetch task to get project_id for file search
|
// Fetch task to get project_id for file search
|
||||||
@@ -58,10 +48,13 @@ export function WorkspacesMainContainer({
|
|||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
containerRef={containerRef}
|
containerRef={containerRef}
|
||||||
projectId={task?.project_id}
|
projectId={task?.project_id}
|
||||||
onViewCode={onViewCode}
|
|
||||||
isNewSessionMode={isNewSessionMode}
|
isNewSessionMode={isNewSessionMode}
|
||||||
onStartNewSession={onStartNewSession}
|
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;
|
filesChanged?: number;
|
||||||
linesAdded?: number;
|
linesAdded?: number;
|
||||||
linesRemoved?: number;
|
linesRemoved?: number;
|
||||||
onViewCode?: () => void;
|
|
||||||
hasConflicts?: boolean;
|
hasConflicts?: boolean;
|
||||||
conflictedFilesCount?: number;
|
conflictedFilesCount?: number;
|
||||||
}
|
}
|
||||||
@@ -135,6 +134,7 @@ interface SessionChatBoxProps {
|
|||||||
executor?: ExecutorProps;
|
executor?: ExecutorProps;
|
||||||
inProgressTodo?: TodoItem | null;
|
inProgressTodo?: TodoItem | null;
|
||||||
localImages?: LocalImageMetadata[];
|
localImages?: LocalImageMetadata[];
|
||||||
|
onViewCode?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -160,6 +160,7 @@ export function SessionChatBox({
|
|||||||
executor,
|
executor,
|
||||||
inProgressTodo,
|
inProgressTodo,
|
||||||
localImages,
|
localImages,
|
||||||
|
onViewCode,
|
||||||
}: SessionChatBoxProps) {
|
}: SessionChatBoxProps) {
|
||||||
const { t } = useTranslation('tasks');
|
const { t } = useTranslation('tasks');
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
@@ -554,7 +555,7 @@ export function SessionChatBox({
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<PrimaryButton variant="tertiary" onClick={stats?.onViewCode}>
|
<PrimaryButton variant="tertiary" onClick={onViewCode}>
|
||||||
<span className="text-sm space-x-half">
|
<span className="text-sm space-x-half">
|
||||||
<span>
|
<span>
|
||||||
{t('diff.filesChanged', { count: filesChanged })}
|
{t('diff.filesChanged', { count: filesChanged })}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import WYSIWYGEditor from '@/components/ui/wysiwyg';
|
import WYSIWYGEditor from '@/components/ui/wysiwyg';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useFileNavigation } from '@/contexts/FileNavigationContext';
|
import { useChangesView } from '@/contexts/ChangesViewContext';
|
||||||
|
|
||||||
interface ChatMarkdownProps {
|
interface ChatMarkdownProps {
|
||||||
content: string;
|
content: string;
|
||||||
@@ -15,7 +15,7 @@ export function ChatMarkdown({
|
|||||||
className,
|
className,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
}: ChatMarkdownProps) {
|
}: ChatMarkdownProps) {
|
||||||
const { viewFileInChanges, findMatchingDiffPath } = useFileNavigation();
|
const { viewFileInChanges, findMatchingDiffPath } = useChangesView();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('text-sm', className)} style={{ maxWidth }}>
|
<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 { cn } from '@/lib/utils';
|
||||||
import { ToolStatus } from 'shared/types';
|
import { ToolStatus } from 'shared/types';
|
||||||
import { ToolStatusDot } from './ToolStatusDot';
|
import { ToolStatusDot } from './ToolStatusDot';
|
||||||
import { useLogNavigation } from '@/contexts/LogNavigationContext';
|
import { useLogsPanel } from '@/contexts/LogsPanelContext';
|
||||||
|
|
||||||
interface ChatScriptEntryProps {
|
interface ChatScriptEntryProps {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -23,7 +23,7 @@ export function ChatScriptEntry({
|
|||||||
onFix,
|
onFix,
|
||||||
}: ChatScriptEntryProps) {
|
}: ChatScriptEntryProps) {
|
||||||
const { t } = useTranslation('tasks');
|
const { t } = useTranslation('tasks');
|
||||||
const { viewProcessInPanel } = useLogNavigation();
|
const { viewProcessInPanel } = useLogsPanel();
|
||||||
const isRunning = status.status === 'created';
|
const isRunning = status.status === 'created';
|
||||||
const isSuccess = status.status === 'success';
|
const isSuccess = status.status === 'success';
|
||||||
const isFailed = status.status === 'failed';
|
const isFailed = status.status === 'failed';
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ interface WorkspacesMainProps {
|
|||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
containerRef: RefObject<HTMLElement | null>;
|
containerRef: RefObject<HTMLElement | null>;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
onViewCode?: () => void;
|
|
||||||
/** Whether user is creating a new session */
|
/** Whether user is creating a new session */
|
||||||
isNewSessionMode?: boolean;
|
isNewSessionMode?: boolean;
|
||||||
/** Callback to start new session mode */
|
/** Callback to start new session mode */
|
||||||
@@ -39,7 +38,6 @@ export function WorkspacesMain({
|
|||||||
isLoading,
|
isLoading,
|
||||||
containerRef,
|
containerRef,
|
||||||
projectId,
|
projectId,
|
||||||
onViewCode,
|
|
||||||
isNewSessionMode,
|
isNewSessionMode,
|
||||||
onStartNewSession,
|
onStartNewSession,
|
||||||
diffStats,
|
diffStats,
|
||||||
@@ -91,7 +89,6 @@ export function WorkspacesMain({
|
|||||||
filesChanged={diffStats?.filesChanged}
|
filesChanged={diffStats?.filesChanged}
|
||||||
linesAdded={diffStats?.linesAdded}
|
linesAdded={diffStats?.linesAdded}
|
||||||
linesRemoved={diffStats?.linesRemoved}
|
linesRemoved={diffStats?.linesRemoved}
|
||||||
onViewCode={onViewCode}
|
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
isNewSessionMode={isNewSessionMode}
|
isNewSessionMode={isNewSessionMode}
|
||||||
onStartNewSession={onStartNewSession}
|
onStartNewSession={onStartNewSession}
|
||||||
|
|||||||
@@ -56,8 +56,6 @@ export function WorkspacesSidebar({
|
|||||||
.filter((workspace) => workspace.name.toLowerCase().includes(searchLower))
|
.filter((workspace) => workspace.name.toLowerCase().includes(searchLower))
|
||||||
.slice(0, isSearching ? undefined : DISPLAY_LIMIT);
|
.slice(0, isSearching ? undefined : DISPLAY_LIMIT);
|
||||||
|
|
||||||
const hasArchivedWorkspaces = archivedWorkspaces.length > 0;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full bg-secondary flex flex-col">
|
<div className="w-full h-full bg-secondary flex flex-col">
|
||||||
{/* Header + Search */}
|
{/* Header + Search */}
|
||||||
@@ -156,7 +154,6 @@ export function WorkspacesSidebar({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Fixed footer toggle - only show if there are archived workspaces */}
|
{/* Fixed footer toggle - only show if there are archived workspaces */}
|
||||||
{hasArchivedWorkspaces && (
|
|
||||||
<div className="border-t border-primary p-base">
|
<div className="border-t border-primary p-base">
|
||||||
<button
|
<button
|
||||||
onClick={() => onShowArchiveChange?.(!showArchive)}
|
onClick={() => onShowArchiveChange?.(!showArchive)}
|
||||||
@@ -178,7 +175,6 @@ export function WorkspacesSidebar({
|
|||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</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 { createContext, useContext, useMemo, type ReactNode } from 'react';
|
||||||
import type {
|
import type { Repo, ExecutorProfileId } from 'shared/types';
|
||||||
Repo,
|
|
||||||
ExecutorProfileId,
|
|
||||||
RepoWithTargetBranch,
|
|
||||||
} from 'shared/types';
|
|
||||||
import { useCreateModeState } from '@/hooks/useCreateModeState';
|
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 {
|
interface CreateModeContextValue {
|
||||||
selectedProjectId: string | null;
|
selectedProjectId: string | null;
|
||||||
@@ -28,16 +27,28 @@ const CreateModeContext = createContext<CreateModeContextValue | null>(null);
|
|||||||
|
|
||||||
interface CreateModeProviderProps {
|
interface CreateModeProviderProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
initialProjectId?: string;
|
|
||||||
initialRepos?: RepoWithTargetBranch[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CreateModeProvider({
|
export function CreateModeProvider({ children }: CreateModeProviderProps) {
|
||||||
children,
|
// Fetch most recent workspace to use as initial values
|
||||||
initialProjectId,
|
const { workspaces: activeWorkspaces, archivedWorkspaces } = useWorkspaces();
|
||||||
initialRepos,
|
const mostRecentWorkspace = activeWorkspaces[0] ?? archivedWorkspaces[0];
|
||||||
}: CreateModeProviderProps) {
|
|
||||||
const state = useCreateModeState({ initialProjectId, initialRepos });
|
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>(
|
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,
|
ReactNode,
|
||||||
useMemo,
|
useMemo,
|
||||||
useCallback,
|
useCallback,
|
||||||
|
useEffect,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { useParams, useNavigate, useLocation } from 'react-router-dom';
|
import { useParams, useNavigate, useLocation } from 'react-router-dom';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
@@ -19,12 +20,17 @@ import {
|
|||||||
useGitHubComments,
|
useGitHubComments,
|
||||||
type NormalizedGitHubComment,
|
type NormalizedGitHubComment,
|
||||||
} from '@/hooks/useGitHubComments';
|
} from '@/hooks/useGitHubComments';
|
||||||
|
import { useDiffStream } from '@/hooks/useDiffStream';
|
||||||
import { attemptsApi } from '@/lib/api';
|
import { attemptsApi } from '@/lib/api';
|
||||||
|
import { useUiPreferencesStore } from '@/stores/useUiPreferencesStore';
|
||||||
|
import { useDiffViewStore } from '@/stores/useDiffViewStore';
|
||||||
import type {
|
import type {
|
||||||
Workspace as ApiWorkspace,
|
Workspace as ApiWorkspace,
|
||||||
Session,
|
Session,
|
||||||
RepoWithTargetBranch,
|
RepoWithTargetBranch,
|
||||||
UnifiedPrComment,
|
UnifiedPrComment,
|
||||||
|
Diff,
|
||||||
|
DiffStats,
|
||||||
} from 'shared/types';
|
} from 'shared/types';
|
||||||
|
|
||||||
export type { NormalizedGitHubComment } from '@/hooks/useGitHubComments';
|
export type { NormalizedGitHubComment } from '@/hooks/useGitHubComments';
|
||||||
@@ -62,6 +68,12 @@ interface WorkspaceContextValue {
|
|||||||
setShowGitHubComments: (show: boolean) => void;
|
setShowGitHubComments: (show: boolean) => void;
|
||||||
getGitHubCommentsForFile: (filePath: string) => NormalizedGitHubComment[];
|
getGitHubCommentsForFile: (filePath: string) => NormalizedGitHubComment[];
|
||||||
getGitHubCommentCountForFile: (filePath: string) => number;
|
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);
|
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
|
// Derive isCreateMode from URL path instead of prop to allow provider to persist across route changes
|
||||||
const isCreateMode = location.pathname === '/workspaces/create';
|
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
|
// Fetch workspaces for sidebar display
|
||||||
const {
|
const {
|
||||||
workspaces: activeWorkspaces,
|
workspaces: activeWorkspaces,
|
||||||
@@ -127,6 +146,30 @@ export function WorkspaceProvider({ children }: WorkspaceProviderProps) {
|
|||||||
enabled: !isCreateMode,
|
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 isLoading = isLoadingList || isLoadingWorkspace;
|
||||||
|
|
||||||
const selectWorkspace = useCallback(
|
const selectWorkspace = useCallback(
|
||||||
@@ -180,6 +223,9 @@ export function WorkspaceProvider({ children }: WorkspaceProviderProps) {
|
|||||||
setShowGitHubComments,
|
setShowGitHubComments,
|
||||||
getGitHubCommentsForFile,
|
getGitHubCommentsForFile,
|
||||||
getGitHubCommentCountForFile,
|
getGitHubCommentCountForFile,
|
||||||
|
diffs,
|
||||||
|
diffPaths,
|
||||||
|
diffStats,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
workspaceId,
|
workspaceId,
|
||||||
@@ -206,6 +252,9 @@ export function WorkspaceProvider({ children }: WorkspaceProviderProps) {
|
|||||||
setShowGitHubComments,
|
setShowGitHubComments,
|
||||||
getGitHubCommentsForFile,
|
getGitHubCommentsForFile,
|
||||||
getGitHubCommentCountForFile,
|
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 { persist } from 'zustand/middleware';
|
||||||
import type { RepoAction } from '@/components/ui-new/primitives/RepoCard';
|
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 =
|
export type ContextBarPosition =
|
||||||
| 'top-left'
|
| 'top-left'
|
||||||
| 'top-right'
|
| 'top-right'
|
||||||
@@ -14,11 +23,9 @@ export type ContextBarPosition =
|
|||||||
// Centralized persist keys for type safety
|
// Centralized persist keys for type safety
|
||||||
export const PERSIST_KEYS = {
|
export const PERSIST_KEYS = {
|
||||||
// Sidebar sections
|
// Sidebar sections
|
||||||
workspacesSidebarActive: 'workspaces-sidebar-active',
|
|
||||||
workspacesSidebarArchived: 'workspaces-sidebar-archived',
|
workspacesSidebarArchived: 'workspaces-sidebar-archived',
|
||||||
// Git panel sections
|
// Git panel sections
|
||||||
gitAdvancedSettings: 'git-advanced-settings',
|
gitAdvancedSettings: 'git-advanced-settings',
|
||||||
gitPanelCreateAddRepo: 'git-panel-create-add-repo',
|
|
||||||
gitPanelRepositories: 'git-panel-repositories',
|
gitPanelRepositories: 'git-panel-repositories',
|
||||||
gitPanelProject: 'git-panel-project',
|
gitPanelProject: 'git-panel-project',
|
||||||
gitPanelAddRepositories: 'git-panel-add-repositories',
|
gitPanelAddRepositories: 'git-panel-add-repositories',
|
||||||
@@ -28,8 +35,6 @@ export const PERSIST_KEYS = {
|
|||||||
changesSection: 'changes-section',
|
changesSection: 'changes-section',
|
||||||
// Preview panel sections
|
// Preview panel sections
|
||||||
devServerSection: 'dev-server-section',
|
devServerSection: 'dev-server-section',
|
||||||
// Context bar
|
|
||||||
contextBarPosition: 'context-bar-position',
|
|
||||||
// GitHub comments toggle
|
// GitHub comments toggle
|
||||||
showGitHubComments: 'show-github-comments',
|
showGitHubComments: 'show-github-comments',
|
||||||
// Panel sizes
|
// Panel sizes
|
||||||
@@ -38,11 +43,12 @@ export const PERSIST_KEYS = {
|
|||||||
repoCard: (repoId: string) => `repo-card-${repoId}` as const,
|
repoCard: (repoId: string) => `repo-card-${repoId}` as const,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
// Check if screen is wide enough to keep sidebar visible
|
||||||
|
const isWideScreen = () => window.innerWidth > 2048;
|
||||||
|
|
||||||
export type PersistKey =
|
export type PersistKey =
|
||||||
| typeof PERSIST_KEYS.workspacesSidebarActive
|
|
||||||
| typeof PERSIST_KEYS.workspacesSidebarArchived
|
| typeof PERSIST_KEYS.workspacesSidebarArchived
|
||||||
| typeof PERSIST_KEYS.gitAdvancedSettings
|
| typeof PERSIST_KEYS.gitAdvancedSettings
|
||||||
| typeof PERSIST_KEYS.gitPanelCreateAddRepo
|
|
||||||
| typeof PERSIST_KEYS.gitPanelRepositories
|
| typeof PERSIST_KEYS.gitPanelRepositories
|
||||||
| typeof PERSIST_KEYS.gitPanelProject
|
| typeof PERSIST_KEYS.gitPanelProject
|
||||||
| typeof PERSIST_KEYS.gitPanelAddRepositories
|
| typeof PERSIST_KEYS.gitPanelAddRepositories
|
||||||
@@ -63,11 +69,21 @@ export type PersistKey =
|
|||||||
| `entry:${string}`;
|
| `entry:${string}`;
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
|
// UI preferences
|
||||||
repoActions: Record<string, RepoAction>;
|
repoActions: Record<string, RepoAction>;
|
||||||
expanded: Record<string, boolean>;
|
expanded: Record<string, boolean>;
|
||||||
contextBarPosition: ContextBarPosition;
|
contextBarPosition: ContextBarPosition;
|
||||||
paneSizes: Record<string, number | string>;
|
paneSizes: Record<string, number | string>;
|
||||||
collapsedPaths: Record<string, 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;
|
setRepoAction: (repoId: string, action: RepoAction) => void;
|
||||||
setExpanded: (key: string, value: boolean) => void;
|
setExpanded: (key: string, value: boolean) => void;
|
||||||
toggleExpanded: (key: string, defaultValue?: boolean) => void;
|
toggleExpanded: (key: string, defaultValue?: boolean) => void;
|
||||||
@@ -75,16 +91,37 @@ type State = {
|
|||||||
setContextBarPosition: (position: ContextBarPosition) => void;
|
setContextBarPosition: (position: ContextBarPosition) => void;
|
||||||
setPaneSize: (key: string, size: number | string) => void;
|
setPaneSize: (key: string, size: number | string) => void;
|
||||||
setCollapsedPaths: (key: string, paths: 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>()(
|
export const useUiPreferencesStore = create<State>()(
|
||||||
persist(
|
persist(
|
||||||
(set) => ({
|
(set, get) => ({
|
||||||
|
// UI preferences state
|
||||||
repoActions: {},
|
repoActions: {},
|
||||||
expanded: {},
|
expanded: {},
|
||||||
contextBarPosition: 'middle-right',
|
contextBarPosition: 'middle-right',
|
||||||
paneSizes: {},
|
paneSizes: {},
|
||||||
collapsedPaths: {},
|
collapsedPaths: {},
|
||||||
|
|
||||||
|
// Layout state
|
||||||
|
isLeftSidebarVisible: true,
|
||||||
|
isLeftMainPanelVisible: true,
|
||||||
|
isRightSidebarVisible: true,
|
||||||
|
rightMainPanelMode: null,
|
||||||
|
previewRefreshKey: 0,
|
||||||
|
|
||||||
|
// UI preferences actions
|
||||||
setRepoAction: (repoId, action) =>
|
setRepoAction: (repoId, action) =>
|
||||||
set((s) => ({ repoActions: { ...s.repoActions, [repoId]: action } })),
|
set((s) => ({ repoActions: { ...s.repoActions, [repoId]: action } })),
|
||||||
setExpanded: (key, value) =>
|
setExpanded: (key, value) =>
|
||||||
@@ -109,8 +146,80 @@ export const useUiPreferencesStore = create<State>()(
|
|||||||
set((s) => ({ paneSizes: { ...s.paneSizes, [key]: size } })),
|
set((s) => ({ paneSizes: { ...s.paneSizes, [key]: size } })),
|
||||||
setCollapsedPaths: (key, paths) =>
|
setCollapsedPaths: (key, paths) =>
|
||||||
set((s) => ({ collapsedPaths: { ...s.collapsedPaths, [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];
|
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 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 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, };
|
export type DirectoryListResponse = { entries: Array<DirectoryEntry>, current_path: string, };
|
||||||
|
|||||||
Reference in New Issue
Block a user