diff --git a/e2e/file-explorer-sidebar.spec.ts b/e2e/file-explorer-sidebar.spec.ts index ff9693c..ce953f6 100644 --- a/e2e/file-explorer-sidebar.spec.ts +++ b/e2e/file-explorer-sidebar.spec.ts @@ -259,9 +259,14 @@ test('git sidebar pane lists grouped working tree changes and opens a diff', asy const gitPane = mainWindow .locator('.sidebar-pane') .filter({ has: mainWindow.locator('.sidebar-pane__title', { hasText: 'Git' }) }) + const worktree = gitPane.locator('.worktrees-panel__worktree').first() + await expect(worktree.locator('.worktrees-panel__worktree-name')).toContainText('git-pane-changes', { + timeout: 6000, + }) + await worktree.locator('.worktrees-panel__worktree-toggle').click() // The Staged Changes group lists the added file with an "A" badge. - const stagedGroup = gitPane.locator('.git-panel__group').filter({ hasText: 'Staged Changes' }) + const stagedGroup = worktree.locator('.git-panel__group').filter({ hasText: 'Staged Changes' }) const stagedRow = stagedGroup.locator('.git-panel__row').filter({ hasText: 'staged-new.txt' }) await expect(stagedRow).toBeVisible({ timeout: 6000 }) await expect(stagedRow.locator('.git-panel__badge')).toHaveText('A') @@ -272,7 +277,7 @@ test('git sidebar pane lists grouped working tree changes and opens a diff', asy await mainWindow.keyboard.press('Escape') // The Changes group lists the modified and untracked files. - const changesGroup = gitPane.locator('.git-panel__group').filter({ hasText: /^Changes/ }) + const changesGroup = worktree.locator('.git-panel__group').filter({ hasText: /^Changes/ }) const modifiedRow = changesGroup.locator('.git-panel__row').filter({ hasText: 'README.md' }) const untrackedRow = changesGroup.locator('.git-panel__row').filter({ hasText: 'untracked.txt' }) await expect(modifiedRow.locator('.git-panel__badge')).toHaveText('M') @@ -281,8 +286,8 @@ test('git sidebar pane lists grouped working tree changes and opens a diff', asy // Modified files are colour-coded amber, matching the file tree. await expect(modifiedRow.locator('.git-panel__icon')).toHaveCSS('color', 'rgb(226, 192, 141)') - // The pane header shows the branch name and a total change count of 3. - await expect(gitPane.locator('.sidebar-pane__count')).toHaveText('3') + // The pane header shows the branch name and the number of worktrees. + await expect(gitPane.locator('.sidebar-pane__count')).toHaveText('1') await expect(gitPane.locator('.sidebar-pane__branch')).toHaveText(/^(main|master)$/) // Clicking a tracked change opens it in the file viewer. @@ -292,9 +297,7 @@ test('git sidebar pane lists grouped working tree changes and opens a diff', asy // Collapsing the Git pane hides the change list... await gitPane.locator('.sidebar-pane__header').click() await expect(gitPane.locator('.git-panel__row')).toHaveCount(0) - // ...but the push-agent button stays available in the collapsed header. await expect(gitPane).toHaveClass(/sidebar-pane--collapsed/) - await expect(gitPane.getByLabel('Commit and push with an AI agent')).toBeVisible() }) test('git sidebar pane shows no changes for a clean repository', async ({ createWorkspace, mainWindow }) => { @@ -319,9 +322,14 @@ test('git sidebar pane shows no changes for a clean repository', async ({ create const gitPane = mainWindow .locator('.sidebar-pane') .filter({ has: mainWindow.locator('.sidebar-pane__title', { hasText: 'Git' }) }) + const worktree = gitPane.locator('.worktrees-panel__worktree').first() + await expect(worktree.locator('.worktrees-panel__worktree-name')).toContainText('git-pane-clean', { + timeout: 6000, + }) + await worktree.locator('.worktrees-panel__worktree-toggle').click() - await expect(gitPane.locator('.git-panel__message')).toHaveText('No changes', { timeout: 6000 }) - await expect(gitPane.locator('.git-panel__row')).toHaveCount(0) + await expect(worktree.locator('.git-panel__message')).toHaveText('No changes', { timeout: 6000 }) + await expect(worktree.locator('.git-panel__row')).toHaveCount(0) }) test('git sidebar pane renders a nested tree and offers a push menu', async ({ @@ -353,27 +361,31 @@ test('git sidebar pane renders a nested tree and offers a push menu', async ({ const gitPane = mainWindow .locator('.sidebar-pane') .filter({ has: mainWindow.locator('.sidebar-pane__title', { hasText: 'Git' }) }) + const worktree = gitPane.locator('.worktrees-panel__worktree').first() + await expect(worktree.locator('.worktrees-panel__worktree-name')).toContainText('git-pane-tree', { + timeout: 6000, + }) + await worktree.locator('.worktrees-panel__worktree-toggle').click() // Tree is the default view: nested folder rows are rendered. - const utilRow = gitPane.locator('.git-panel__row').filter({ hasText: 'util.ts' }) + const utilRow = worktree.locator('.git-panel__row').filter({ hasText: 'util.ts' }) await expect(utilRow).toBeVisible({ timeout: 6000 }) - // A single section has no redundant group header (the "Git" pane header covers it). - await expect(gitPane.locator('.git-panel__group-header')).toHaveCount(0) - await expect(gitPane.locator('.git-panel__folder-name').filter({ hasText: 'src' })).toBeVisible() - await expect(gitPane.locator('.git-panel__folder-name').filter({ hasText: 'lib' })).toBeVisible() + // A single section has no redundant group header (the worktree row covers it). + await expect(worktree.locator('.git-panel__group-header')).toHaveCount(0) + await expect(worktree.locator('.git-panel__folder-name').filter({ hasText: 'src' })).toBeVisible() + await expect(worktree.locator('.git-panel__folder-name').filter({ hasText: 'lib' })).toBeVisible() // In tree mode the row does not repeat the directory path. await expect(utilRow.locator('.git-panel__dir')).toHaveCount(0) // Collapsing the "lib" folder hides its files. - await gitPane.locator('.git-panel__folder').filter({ hasText: 'lib' }).click() + await worktree.locator('.git-panel__folder').filter({ hasText: 'lib' }).click() await expect(utilRow).toHaveCount(0) // Re-expand. - await gitPane.locator('.git-panel__folder').filter({ hasText: 'lib' }).click() + await worktree.locator('.git-panel__folder').filter({ hasText: 'lib' }).click() await expect(utilRow).toBeVisible() - // The sidebar header exposes a push-agent menu (replacing the old - // tree/list toggle) offering the four commit-and-push actions. - const pushButton = gitPane.getByLabel('Commit and push with an AI agent') + // The worktree row exposes a push-agent menu offering the four commit-and-push actions. + const pushButton = worktree.getByLabel(/Commit and push .* with an AI agent/) await expect(pushButton).toBeVisible() await pushButton.click() diff --git a/src/App.tsx b/src/App.tsx index bd06718..52e91c4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -39,7 +39,6 @@ import { Sparkles, Terminal, Trash2, - Upload, Zap, } from 'lucide-react'; import { ContextMenu, type ContextMenuItem } from './components/ContextMenu'; @@ -47,14 +46,9 @@ import type { FilePanelInstanceParams } from './components/file-viewer'; import { FilePanel, FileTab } from './components/file-viewer'; import type { FolderPanelInstanceParams } from './components/folder-viewer'; import { FolderPanel, FolderTab } from './components/folder-viewer'; -import { GitPanel } from './components/git-panel/GitPanel'; import { WorktreesPanel } from './components/git-panel/WorktreesPanel'; import { SidebarPane } from './components/sidebar/SidebarPane'; -import { - SIDEBAR_HEADER_MIN_HEIGHT, - SIDEBAR_SPLITTER_HEIGHT, - SidebarSplit, -} from './components/sidebar/SidebarSplit'; +import { SidebarSplit } from './components/sidebar/SidebarSplit'; import { McpInstallModal } from './components/McpInstallModal'; import { QuickPushModal } from './components/QuickPushModal'; import type { @@ -110,7 +104,6 @@ import type { FileSearchResult, GitChangeEntry, GitWorktreeStatus, - GitPanelStatus, RemoteAccessStatus, TerminalRecordingStartMetadata, TerminalRecordingState, @@ -420,9 +413,7 @@ type ProjectTab = { isFileExplorerOpen: boolean; isExplorerPaneCollapsed: boolean; isGitPaneCollapsed: boolean; - isWorktreesPaneCollapsed: boolean; sidebarExplorerHeight: number; - sidebarGitHeight: number; rootFolder: string; }; @@ -711,10 +702,7 @@ function createProjectTab( isFileExplorerOpen: false, isExplorerPaneCollapsed: sidebarDefaults.defaultExplorerState === 'collapsed', isGitPaneCollapsed: sidebarDefaults.defaultGitState === 'collapsed', - isWorktreesPaneCollapsed: - sidebarDefaults.defaultWorktreesState === 'collapsed', sidebarExplorerHeight: sidebarDefaults.defaultExplorerPaneHeight, - sidebarGitHeight: sidebarDefaults.defaultGitPaneHeight, rootFolder: homePath, }; } @@ -1960,13 +1948,10 @@ const ProjectWorkspace = forwardRef< const [gitStatuses, setGitStatuses] = useState< Record >({}); - const [gitPanelStatus, setGitPanelStatus] = useState( - null, - ); const [worktreePanelStatus, setWorktreePanelStatus] = useState(null); const [gitPushMenuPosition, setGitPushMenuPosition] = useState<{ - target?: GitPushMenuTarget; + target: GitPushMenuTarget; x: number; y: number; } | null>(null); @@ -1983,6 +1968,19 @@ const ProjectWorkspace = forwardRef< const fileExplorerNameDialogRequestIdRef = useRef(0); const [fileExplorerNameDialog, setFileExplorerNameDialog] = useState(null); + const currentGitBranch = useMemo(() => { + const worktrees = worktreePanelStatus?.worktrees; + if (!worktrees) { + return null; + } + + return ( + worktrees.find((worktree) => worktree.isCurrent)?.branch ?? + worktrees.find((worktree) => worktree.path === worktreePanelStatus.repoRoot) + ?.branch ?? + null + ); + }, [worktreePanelStatus]); const [isDockviewReady, setIsDockviewReady] = useState(false); const [isMacroLauncherOpen, setIsMacroLauncherOpen] = useState(false); @@ -2969,7 +2967,6 @@ const ProjectWorkspace = forwardRef< const refreshGitStatuses = useCallback(async () => { if (!project.rootFolder) { setGitStatuses({}); - setGitPanelStatus(null); setWorktreePanelStatus(null); return; } @@ -2980,17 +2977,14 @@ const ProjectWorkspace = forwardRef< isRefreshingGitStatusesRef.current = true; try { - const [nextStatuses, nextPanel, nextWorktrees] = await Promise.all([ + const [nextStatuses, nextWorktrees] = await Promise.all([ window.terminay.getFileExplorerGitStatuses(project.rootFolder), - window.terminay.getGitPanelStatus(project.rootFolder), window.terminay.getWorktreePanelStatus(project.rootFolder), ]); setGitStatuses(nextStatuses.statuses); - setGitPanelStatus(nextPanel); setWorktreePanelStatus(nextWorktrees); } catch { setGitStatuses({}); - setGitPanelStatus(null); setWorktreePanelStatus(null); } finally { isRefreshingGitStatusesRef.current = false; @@ -4797,7 +4791,7 @@ const ProjectWorkspace = forwardRef< ); const launchGitPushAgent = useCallback( - (action: GitPushAgentAction, target?: GitPushMenuTarget) => { + (action: GitPushAgentAction, target: GitPushMenuTarget) => { setGitPushMenuPosition(null); const config = settings.gitPushAgent; @@ -4825,7 +4819,7 @@ const ProjectWorkspace = forwardRef< const prompt = buildGitPushAgentPrompt( config.prompt, actionMeta.task, - target ? target.branch : gitPanelStatus?.branch, + target.branch, ); const command = buildGitPushAgentCommand( config.provider, @@ -4834,21 +4828,16 @@ const ProjectWorkspace = forwardRef< ); void addTerminal({ - cwd: target?.cwd ?? project.rootFolder, + cwd: target.cwd, title: 'Push agent', initialInput: command, }); }, - [ - addTerminal, - gitPanelStatus?.branch, - project.rootFolder, - settings.gitPushAgent, - ], + [addTerminal, settings.gitPushAgent], ); const launchQuickPush = useCallback( - async (action: GitPushAgentAction, target?: GitPushMenuTarget) => { + async (action: GitPushAgentAction, target: GitPushMenuTarget) => { setGitPushMenuPosition(null); if (settings.gitPushAgent.provider === 'disabled') { @@ -4861,27 +4850,10 @@ const ProjectWorkspace = forwardRef< return; } - let cwd = target?.cwd ?? project.rootFolder; - if (!target) { - // Operate on the active terminal's working directory so Quick Push - // targets the branch the user is actually on (e.g. a worktree), - // falling back to the project root. - const activeSessionId = getActiveSessionId(); - if (activeSessionId) { - try { - cwd = - (await window.terminay.getTerminalCwd(activeSessionId)) ?? - project.rootFolder; - } catch { - cwd = project.rootFolder; - } - } - } - - setQuickPushCwd(cwd); + setQuickPushCwd(target.cwd); setQuickPushAction(action); }, - [settings.gitPushAgent.provider, project.rootFolder, getActiveSessionId], + [settings.gitPushAgent.provider], ); const buildMovedTerminalFromPanel = useCallback( @@ -6986,18 +6958,9 @@ const ProjectWorkspace = forwardRef< > { onUpdateProject(project.id, { sidebarExplorerHeight: height, @@ -7056,117 +7019,43 @@ const ProjectWorkspace = forwardRef< } bottom={ - { + { + const next = !project.isGitPaneCollapsed; onUpdateProject(project.id, { - sidebarGitHeight: height, + isGitPaneCollapsed: next, }); - }} - onTopHeightCommit={(height) => { updateSidebarSettings({ - defaultGitPaneHeight: height, + defaultGitState: next ? 'collapsed' : 'expanded', }); }} - top={ - { - const next = !project.isGitPaneCollapsed; - onUpdateProject(project.id, { - isGitPaneCollapsed: next, - }); - updateSidebarSettings({ - defaultGitState: next ? 'collapsed' : 'expanded', - }); - }} - count={gitPanelStatus?.entries.length} - accessory={ - gitPanelStatus?.branch ? ( - - {gitPanelStatus.branch} - - ) : null - } - actions={ - - } - > - - - } - bottom={ - { - const next = !project.isWorktreesPaneCollapsed; - onUpdateProject(project.id, { - isWorktreesPaneCollapsed: next, - }); - updateSidebarSettings({ - defaultWorktreesState: next - ? 'collapsed' - : 'expanded', - }); - }} - count={worktreePanelStatus?.worktrees.length} - > - - + count={worktreePanelStatus?.worktrees.length} + accessory={ + currentGitBranch ? ( + + {currentGitBranch} + + ) : null } - /> + > + + } /> diff --git a/src/components/git-panel/gitPanel.css b/src/components/git-panel/gitPanel.css index e9f1b59..f6dd5d0 100644 --- a/src/components/git-panel/gitPanel.css +++ b/src/components/git-panel/gitPanel.css @@ -365,7 +365,6 @@ color: rgba(220, 226, 240, 0.34); font-size: 11px; line-height: 16px; - padding-left: 22px; } .worktrees-panel__pill { @@ -449,8 +448,8 @@ display: flex; flex-direction: column; min-height: 0; - margin-left: 28px; - padding-left: 10px; + margin-left: 18px; + padding-left: 4px; border-left: 1px solid rgba(220, 226, 240, 0.08); } diff --git a/src/terminalSettings.ts b/src/terminalSettings.ts index 1669bc2..ec86615 100644 --- a/src/terminalSettings.ts +++ b/src/terminalSettings.ts @@ -399,10 +399,8 @@ export const defaultTerminalSettings: TerminalSettings = { gitPanelViewMode: 'tree', defaultExplorerState: 'expanded', defaultGitState: 'expanded', - defaultWorktreesState: 'expanded', defaultWidth: 280, defaultExplorerPaneHeight: 320, - defaultGitPaneHeight: 180, }, theme: { foreground: '#dce2f0', @@ -1190,20 +1188,6 @@ export const terminalSettingsSections: SettingsSectionDefinition[] = [ ], keywords: ['sidebar', 'git', 'collapse', 'expand', 'default'], }), - makeField({ - key: 'sidebar.defaultWorktreesState', - label: 'Default Worktrees state', - description: - 'Whether the Worktrees pane starts expanded or collapsed in new projects.', - sectionId: 'sidebar', - categoryId: 'files', - input: 'select', - options: [ - { label: 'Expanded', value: 'expanded' }, - { label: 'Collapsed', value: 'collapsed' }, - ], - keywords: ['sidebar', 'worktrees', 'collapse', 'expand', 'default'], - }), makeField({ key: 'sidebar.defaultWidth', label: 'Default sidebar width', @@ -1237,19 +1221,6 @@ export const terminalSettingsSections: SettingsSectionDefinition[] = [ 'default', ], }), - makeField({ - key: 'sidebar.defaultGitPaneHeight', - label: 'Default Git pane height', - description: - 'Initial height in pixels of the Git pane above the Worktrees pane in new projects.', - sectionId: 'sidebar', - categoryId: 'files', - input: 'number', - min: 80, - max: 1000, - step: 10, - keywords: ['sidebar', 'git', 'worktrees', 'height', 'size', 'default'], - }), ], }, { @@ -2663,11 +2634,6 @@ export function normalizeTerminalSettings( sidebarInput.defaultGitState === 'expanded' ? sidebarInput.defaultGitState : defaultTerminalSettings.sidebar.defaultGitState, - defaultWorktreesState: - sidebarInput.defaultWorktreesState === 'collapsed' || - sidebarInput.defaultWorktreesState === 'expanded' - ? sidebarInput.defaultWorktreesState - : defaultTerminalSettings.sidebar.defaultWorktreesState, defaultWidth: clampNumber( Number(sidebarInput.defaultWidth), defaultTerminalSettings.sidebar.defaultWidth, @@ -2680,12 +2646,6 @@ export function normalizeTerminalSettings( 80, 2000, ), - defaultGitPaneHeight: clampNumber( - Number(sidebarInput.defaultGitPaneHeight), - defaultTerminalSettings.sidebar.defaultGitPaneHeight, - 80, - 2000, - ), }, theme: { foreground: diff --git a/src/types/settings.ts b/src/types/settings.ts index 83223e2..22dd32c 100644 --- a/src/types/settings.ts +++ b/src/types/settings.ts @@ -68,10 +68,8 @@ export type SidebarSettings = { gitPanelViewMode: GitPanelViewMode; defaultExplorerState: SidebarPaneState; defaultGitState: SidebarPaneState; - defaultWorktreesState: SidebarPaneState; defaultWidth: number; defaultExplorerPaneHeight: number; - defaultGitPaneHeight: number; }; export type FileViewerSettings = {